plugin-gentleman 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.
- package/README.md +315 -0
- package/gentleman.json +55 -0
- package/package.json +45 -0
- package/tui.tsx +305 -0
package/README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# plugin-gentleman
|
|
2
|
+
|
|
3
|
+
OpenCode TUI plugin featuring **Mustachi** — an animated ASCII mascot with eyes, mustache, and optional motivational phrases during busy states.
|
|
4
|
+
|
|
5
|
+
## What This Is
|
|
6
|
+
|
|
7
|
+
A TUI plugin for OpenCode that:
|
|
8
|
+
- 🎭 Shows **Mustachi** (ASCII mascot) in your home logo
|
|
9
|
+
- 👀 Subtle **eye animations** (optional, low-frequency)
|
|
10
|
+
- 💬 **Motivational phrases** during busy/loading states (Rioplatense Spanish style)
|
|
11
|
+
- 🎨 Installs and applies the **Gentleman theme** automatically
|
|
12
|
+
- 🖥️ Detects and displays your **OS and LLM providers**
|
|
13
|
+
- ⚙️ Fully **configurable** via `opencode.json`
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### Method 1: Local Testing with Tarball (Recommended First)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm pack
|
|
21
|
+
npm install -g ./plugin-gentleman-1.0.0.tgz
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Add to `~/.config/opencode/opencode.json`:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"$schema": "https://opencode.ai/config.json",
|
|
29
|
+
"plugin": ["plugin-gentleman"]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Restart OpenCode:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
opencode
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Method 2: Development with npm link
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm link
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Add to `~/.config/opencode/opencode.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"$schema": "https://opencode.ai/config.json",
|
|
50
|
+
"plugin": ["plugin-gentleman"]
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Restart OpenCode.
|
|
55
|
+
|
|
56
|
+
### Method 3: npm Registry (After Publishing)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install -g plugin-gentleman
|
|
60
|
+
opencode plugin install plugin-gentleman
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
### Mustachi Mascot
|
|
66
|
+
|
|
67
|
+
Mustachi is an ASCII character with:
|
|
68
|
+
- Two large eyes that occasionally look in different directions
|
|
69
|
+
- A prominent mustache rendered in theme colors
|
|
70
|
+
- A tongue that appears during busy/loading states
|
|
71
|
+
- Motivational phrases in Rioplatense Spanish style
|
|
72
|
+
|
|
73
|
+
Example phrases shown during busy states:
|
|
74
|
+
- "Ponete las pilas, hermano..."
|
|
75
|
+
- "Dale que va, dale que va..."
|
|
76
|
+
- "Ya casi, ya casi..."
|
|
77
|
+
- "Ahí vamos, loco..."
|
|
78
|
+
- "Un toque más y listo..."
|
|
79
|
+
- "Aguantá que estoy pensando..."
|
|
80
|
+
|
|
81
|
+
### Animation Behavior
|
|
82
|
+
|
|
83
|
+
**Low complexity, low frequency** — designed to be subtle and non-intrusive:
|
|
84
|
+
|
|
85
|
+
1. **Eye Direction Animation** (when `animations: true`)
|
|
86
|
+
- Every ~4 seconds, Mustachi's eyes may look in a random direction
|
|
87
|
+
- 20% chance of eye movement per interval
|
|
88
|
+
- Returns to neutral position most of the time
|
|
89
|
+
|
|
90
|
+
2. **Busy State Animation** (when `animations: true`)
|
|
91
|
+
- Tongue appears when OpenCode is processing
|
|
92
|
+
- Motivational phrase rotates every 3 seconds
|
|
93
|
+
- Only active during detected busy states
|
|
94
|
+
|
|
95
|
+
3. **No Animation** (when `animations: false`)
|
|
96
|
+
- Mustachi stays in neutral position
|
|
97
|
+
- No eye movement
|
|
98
|
+
- No tongue or phrases during busy states
|
|
99
|
+
|
|
100
|
+
### Theme
|
|
101
|
+
|
|
102
|
+
The plugin bundles the **Gentleman theme** with a refined dark color palette:
|
|
103
|
+
- Background: Deep navy (`#06080f`)
|
|
104
|
+
- Primary: Soft blue (`#7FB4CA`)
|
|
105
|
+
- Accent: Warm gold (`#E0C15A`)
|
|
106
|
+
- Text: Clean white (`#F3F6F9`)
|
|
107
|
+
|
|
108
|
+
Mustachi uses a 3-tone gradient:
|
|
109
|
+
- **Top**: Accent (gold)
|
|
110
|
+
- **Middle**: Primary (blue)
|
|
111
|
+
- **Bottom**: Error/pink (`#CB7C94`)
|
|
112
|
+
|
|
113
|
+
### Environment Detection
|
|
114
|
+
|
|
115
|
+
The plugin shows a "Detected" line with:
|
|
116
|
+
- **OS Detection**: Reads distro name on Linux, shows "macOS" or "Windows" on other platforms
|
|
117
|
+
- **Provider Detection**: Lists active LLM providers (OpenAI, Copilot, Google, etc.)
|
|
118
|
+
|
|
119
|
+
Both are fully configurable and can be hidden.
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
All options are set via plugin tuple syntax in `opencode.json`:
|
|
124
|
+
|
|
125
|
+
### Default Configuration
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"$schema": "https://opencode.ai/config.json",
|
|
130
|
+
"plugin": [
|
|
131
|
+
[
|
|
132
|
+
"plugin-gentleman",
|
|
133
|
+
{
|
|
134
|
+
"enabled": true,
|
|
135
|
+
"theme": "gentleman",
|
|
136
|
+
"set_theme": true,
|
|
137
|
+
"show_detected": true,
|
|
138
|
+
"show_os": true,
|
|
139
|
+
"show_providers": true,
|
|
140
|
+
"animations": true
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Configuration Options
|
|
148
|
+
|
|
149
|
+
| Option | Type | Default | Description |
|
|
150
|
+
|--------|------|---------|-------------|
|
|
151
|
+
| `enabled` | boolean | `true` | Enable/disable the plugin entirely |
|
|
152
|
+
| `theme` | string | `"gentleman"` | Name of the bundled theme to install |
|
|
153
|
+
| `set_theme` | boolean | `true` | Automatically activate the theme on load |
|
|
154
|
+
| `show_detected` | boolean | `true` | Show the "Detected" environment info line |
|
|
155
|
+
| `show_os` | boolean | `true` | Show detected operating system name |
|
|
156
|
+
| `show_providers` | boolean | `true` | Show detected LLM providers |
|
|
157
|
+
| `animations` | boolean | `true` | Enable Mustachi animations (eyes, busy state) |
|
|
158
|
+
|
|
159
|
+
### Example: Disable Animations
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"$schema": "https://opencode.ai/config.json",
|
|
164
|
+
"plugin": [
|
|
165
|
+
["plugin-gentleman", { "animations": false }]
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Mustachi will remain static with neutral eyes and no busy-state expressions.
|
|
171
|
+
|
|
172
|
+
### Example: Logo Only (No Detection)
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"$schema": "https://opencode.ai/config.json",
|
|
177
|
+
"plugin": [
|
|
178
|
+
["plugin-gentleman", { "show_detected": false }]
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Shows only Mustachi and the OpenCode branding, no OS/provider info.
|
|
184
|
+
|
|
185
|
+
### Example: Show Only OS
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"$schema": "https://opencode.ai/config.json",
|
|
190
|
+
"plugin": [
|
|
191
|
+
[
|
|
192
|
+
"plugin-gentleman",
|
|
193
|
+
{
|
|
194
|
+
"show_detected": true,
|
|
195
|
+
"show_os": true,
|
|
196
|
+
"show_providers": false
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## How It Works
|
|
204
|
+
|
|
205
|
+
1. **Theme Installation**: On load, installs `gentleman.json` into OpenCode themes
|
|
206
|
+
2. **Theme Activation**: If `set_theme: true`, switches to the gentleman theme
|
|
207
|
+
3. **Logo Slot**: Registers `home_logo` slot with Mustachi ASCII art
|
|
208
|
+
4. **Environment Detection Slot**: Registers `home_prompt_after` slot with OS/provider info
|
|
209
|
+
5. **Animation Loop**: If `animations: true`, starts interval timers for eye variations and busy-state detection
|
|
210
|
+
6. **Busy State Detection**: Attempts to read `api.state.session.running` (best-effort; may not be exposed by all OpenCode versions)
|
|
211
|
+
|
|
212
|
+
### Technical Details
|
|
213
|
+
|
|
214
|
+
- **No Build Step**: Plain TSX transpiled at runtime by OpenCode
|
|
215
|
+
- **Solid.js Reactivity**: Uses `createSignal` and `createEffect` for animations
|
|
216
|
+
- **Safe Detection**: All OS/provider detection wrapped in try-catch blocks
|
|
217
|
+
- **Cleanup**: Uses `onCleanup` to clear intervals when component unmounts
|
|
218
|
+
|
|
219
|
+
## Supported Providers
|
|
220
|
+
|
|
221
|
+
The plugin maps these provider IDs to friendly names:
|
|
222
|
+
|
|
223
|
+
| Provider ID | Display Name |
|
|
224
|
+
|-------------|--------------|
|
|
225
|
+
| `openai` | OpenAI |
|
|
226
|
+
| `google` | Google |
|
|
227
|
+
| `github-copilot` | Copilot |
|
|
228
|
+
| `opencode-go` | OpenCode GO |
|
|
229
|
+
| `anthropic` | Claude |
|
|
230
|
+
| `deepseek` | DeepSeek |
|
|
231
|
+
| `openrouter` | OpenRouter |
|
|
232
|
+
| `mistral` | Mistral |
|
|
233
|
+
| `groq` | Groq |
|
|
234
|
+
| `cohere` | Cohere |
|
|
235
|
+
| `together` | Together |
|
|
236
|
+
| `perplexity` | Perplexity |
|
|
237
|
+
|
|
238
|
+
Unknown provider IDs display the configured name or raw ID.
|
|
239
|
+
|
|
240
|
+
## Local Testing Limitations
|
|
241
|
+
|
|
242
|
+
**IMPORTANT**: This is a **TUI plugin** for npm installation.
|
|
243
|
+
|
|
244
|
+
If you try to use this as a local system plugin (copying `.ts` files to `~/.config/opencode/plugins/`):
|
|
245
|
+
- ❌ **NO visual changes** — system plugins cannot modify the TUI
|
|
246
|
+
- ❌ **NO Mustachi** — only TUI plugins can register slot components
|
|
247
|
+
- ❌ **NO animations** — JSX/Solid.js components only work in TUI plugins
|
|
248
|
+
|
|
249
|
+
**For full features, use npm installation** (tarball or link method above).
|
|
250
|
+
|
|
251
|
+
## Repository Structure
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
oc-plugin-gentleman/
|
|
255
|
+
├── tui.tsx # TUI plugin entry point (main implementation) ✅ npm
|
|
256
|
+
├── gentleman.json # Gentleman theme definition ✅ npm
|
|
257
|
+
├── package.json # npm package manifest with exports ✅ npm
|
|
258
|
+
├── README.md # This file ✅ npm (auto-included)
|
|
259
|
+
├── gentleman-local.ts # Legacy local system plugin (repo-only, not in npm package)
|
|
260
|
+
├── install-local-real.sh # Install script for local plugin (repo-only)
|
|
261
|
+
├── install-local.sh # Install script for local plugin (repo-only)
|
|
262
|
+
└── mustachi examples/ # PNG reference images (repo-only)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Files included in npm package** (via `files` field in package.json):
|
|
266
|
+
- `tui.tsx` — main plugin implementation
|
|
267
|
+
- `gentleman.json` — theme definition
|
|
268
|
+
- `package.json` — auto-included by npm
|
|
269
|
+
- `README.md` — auto-included by npm
|
|
270
|
+
|
|
271
|
+
**Local-only files** (excluded from npm, kept in repo for development/reference):
|
|
272
|
+
- `gentleman-local.ts` — legacy local system plugin with limited features
|
|
273
|
+
- `install-local-real.sh`, `install-local.sh` — local installation scripts
|
|
274
|
+
- `mustachi examples/` — reference images
|
|
275
|
+
oc-plugin-gentleman/
|
|
276
|
+
├── tui.tsx # TUI plugin entry point (main implementation)
|
|
277
|
+
├── gentleman.json # Gentleman theme definition
|
|
278
|
+
├── package.json # npm package manifest with exports
|
|
279
|
+
├── gentleman-local.ts # Legacy local system plugin (limited features)
|
|
280
|
+
├── install-local-real.sh # Install script for local system plugin
|
|
281
|
+
├── mustachi examples/ # PNG reference images (not used in final plugin)
|
|
282
|
+
└── README.md # This file
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Caveats Before Publishing
|
|
286
|
+
|
|
287
|
+
1. **Busy State Detection**: The plugin attempts to detect busy states via `api.state.session.running`, but this may not be exposed in all OpenCode versions. If unavailable, busy-state animations won't trigger (eye animations still work).
|
|
288
|
+
|
|
289
|
+
2. **Animation Frequency**: Currently set to low frequency (4s for eyes, 3s for phrase rotation). If this feels too fast or too slow in real usage, adjust the intervals in `tui.tsx`.
|
|
290
|
+
|
|
291
|
+
3. **Sidebar Support**: OpenCode TUI may or may not support sidebar slots. Currently, Mustachi only appears in the `home_logo` slot. If sidebar slots become available, this can be added in a future version.
|
|
292
|
+
|
|
293
|
+
4. **Theme Compatibility**: The plugin installs and optionally activates the Gentleman theme. If the user has a custom theme they prefer, they should set `set_theme: false`.
|
|
294
|
+
|
|
295
|
+
## Development
|
|
296
|
+
|
|
297
|
+
To modify the plugin:
|
|
298
|
+
|
|
299
|
+
1. Edit `tui.tsx` (main implementation)
|
|
300
|
+
2. Test locally with `npm link` or `npm pack`
|
|
301
|
+
3. No build step needed — OpenCode transpiles TSX at runtime
|
|
302
|
+
4. Restart OpenCode to see changes
|
|
303
|
+
|
|
304
|
+
To add new eye variations:
|
|
305
|
+
- Edit the `eyeVariations` array in `tui.tsx`
|
|
306
|
+
|
|
307
|
+
To add new busy phrases:
|
|
308
|
+
- Edit the `busyPhrases` array in `tui.tsx`
|
|
309
|
+
|
|
310
|
+
To change animation timings:
|
|
311
|
+
- Adjust `setInterval` durations in the `Home` component
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
ISC
|
package/gentleman.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/theme.json",
|
|
3
|
+
"theme": {
|
|
4
|
+
"background": "none",
|
|
5
|
+
"backgroundPanel": "#06080f",
|
|
6
|
+
"backgroundElement": "#06080f",
|
|
7
|
+
"text": "#F3F6F9",
|
|
8
|
+
"textMuted": "#5C6170",
|
|
9
|
+
"primary": "#7FB4CA",
|
|
10
|
+
"secondary": "#A3B5D6",
|
|
11
|
+
"accent": "#E0C15A",
|
|
12
|
+
"error": "#CB7C94",
|
|
13
|
+
"warning": "#DEBA87",
|
|
14
|
+
"success": "#B7CC85",
|
|
15
|
+
"info": "#7FB4CA",
|
|
16
|
+
"border": "#313342",
|
|
17
|
+
"borderActive": "#7FB4CA",
|
|
18
|
+
"borderSubtle": "#232A40",
|
|
19
|
+
"diffAdded": "#B7CC85",
|
|
20
|
+
"diffRemoved": "#CB7C94",
|
|
21
|
+
"diffContext": "#5C6170",
|
|
22
|
+
"diffHunkHeader": "#8394A3",
|
|
23
|
+
"diffHighlightAdded": "#D1E8A9",
|
|
24
|
+
"diffHighlightRemoved": "#DE8FA8",
|
|
25
|
+
"diffAddedBg": "#1a2e1a",
|
|
26
|
+
"diffRemovedBg": "#2e1a1a",
|
|
27
|
+
"diffContextBg": "#0d0f14",
|
|
28
|
+
"diffLineNumber": "#8394A3",
|
|
29
|
+
"diffAddedLineNumberBg": "#1a2e1a",
|
|
30
|
+
"diffRemovedLineNumberBg": "#2e1a1a",
|
|
31
|
+
"markdownText": "#F3F6F9",
|
|
32
|
+
"markdownHeading": "#B5B2D0",
|
|
33
|
+
"markdownLink": "#7FB4CA",
|
|
34
|
+
"markdownLinkText": "#79B8EA",
|
|
35
|
+
"markdownCode": "#B7CC85",
|
|
36
|
+
"markdownBlockQuote": "#DEBA87",
|
|
37
|
+
"markdownEmph": "#7CB9DD",
|
|
38
|
+
"markdownStrong": "#DEBA87",
|
|
39
|
+
"markdownHorizontalRule": "#5C6170",
|
|
40
|
+
"markdownListItem": "#7FB4CA",
|
|
41
|
+
"markdownListEnumeration": "#A3B5D6",
|
|
42
|
+
"markdownImage": "#7FB4CA",
|
|
43
|
+
"markdownImageText": "#79B8EA",
|
|
44
|
+
"markdownCodeBlock": "#F3F6F9",
|
|
45
|
+
"syntaxComment": "#8394A3",
|
|
46
|
+
"syntaxKeyword": "#C99AD6",
|
|
47
|
+
"syntaxFunction": "#B99BF2",
|
|
48
|
+
"syntaxVariable": "#F3F6F9",
|
|
49
|
+
"syntaxString": "#DFBD76",
|
|
50
|
+
"syntaxNumber": "#A4DAA7",
|
|
51
|
+
"syntaxType": "#8FB8DD",
|
|
52
|
+
"syntaxOperator": "#DEBA87",
|
|
53
|
+
"syntaxPunctuation": "#96A2B0"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "plugin-gentleman",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "OpenCode TUI plugin featuring Mustachi - an animated ASCII mascot with eyes, mustache, and optional motivational phrases during busy states",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./tui": {
|
|
9
|
+
"import": "./tui.tsx",
|
|
10
|
+
"config": {
|
|
11
|
+
"enabled": true,
|
|
12
|
+
"theme": "gentleman",
|
|
13
|
+
"set_theme": true,
|
|
14
|
+
"show_detected": true,
|
|
15
|
+
"show_os": true,
|
|
16
|
+
"show_providers": true,
|
|
17
|
+
"animations": true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"tui.tsx",
|
|
23
|
+
"gentleman.json"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"opencode": ">=1.3.13"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@opencode-ai/plugin": "*",
|
|
30
|
+
"@opentui/core": "*",
|
|
31
|
+
"@opentui/solid": "*",
|
|
32
|
+
"solid-js": "*"
|
|
33
|
+
},
|
|
34
|
+
"keywords": ["opencode", "plugin", "theme", "gentleman", "tui", "ascii", "mustachi"],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "ISC",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/IrrealV/plugin-gentleman.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/IrrealV/plugin-gentleman#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/IrrealV/plugin-gentleman/issues"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/tui.tsx
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/** @jsxImportSource @opentui/solid */
|
|
3
|
+
import { readFileSync } from "node:fs"
|
|
4
|
+
import type { TuiPlugin, TuiPluginModule, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
5
|
+
import { createSignal, onCleanup, createEffect } from "solid-js"
|
|
6
|
+
|
|
7
|
+
const id = "gentleman"
|
|
8
|
+
|
|
9
|
+
// Mustachi ASCII art - inspired by mustachi examples (eyes, mustache, optional tongue)
|
|
10
|
+
// Base state: neutral look
|
|
11
|
+
const mustachiBase = [
|
|
12
|
+
"",
|
|
13
|
+
" ╭─────╮ ╭─────╮ ",
|
|
14
|
+
" │ ● ● │ │ ○ ○ │ ",
|
|
15
|
+
" ╰─────╯ ╰─────╯ ",
|
|
16
|
+
" ╲ ╱ ",
|
|
17
|
+
" ╲ ╱ ",
|
|
18
|
+
" ╭─────═════════─────╮ ",
|
|
19
|
+
" ╱ ╭───────────╮ ╲ ",
|
|
20
|
+
" ╱ │ ~~~~~~~~~ │ ╲ ",
|
|
21
|
+
"╱ ╰───────────╯ ╲ ",
|
|
22
|
+
"╲ ╱ ",
|
|
23
|
+
" ╲ ╱ ",
|
|
24
|
+
" ╰───────────────────╯ ",
|
|
25
|
+
"",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
// Eye variations for subtle animation
|
|
29
|
+
const eyeVariations = [
|
|
30
|
+
{ left: "● ●", right: "○ ○" }, // neutral
|
|
31
|
+
{ left: "◐ ●", right: "○ ○" }, // left eye looking left
|
|
32
|
+
{ left: "● ◑", right: "○ ○" }, // left eye looking right
|
|
33
|
+
{ left: "● ●", right: "◐ ○" }, // right eye looking left
|
|
34
|
+
{ left: "● ●", right: "○ ◑" }, // right eye looking right
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
// Busy/loading state with tongue and motivational phrases
|
|
38
|
+
const busyPhrases = [
|
|
39
|
+
"Ponete las pilas, hermano...",
|
|
40
|
+
"Dale que va, dale que va...",
|
|
41
|
+
"Ya casi, ya casi...",
|
|
42
|
+
"Ahí vamos, loco...",
|
|
43
|
+
"Un toque más y listo...",
|
|
44
|
+
"Aguantá que estoy pensando...",
|
|
45
|
+
"Momento, momento...",
|
|
46
|
+
"Ya te lo traigo, tranqui...",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
type Cfg = {
|
|
50
|
+
enabled: boolean
|
|
51
|
+
theme: string
|
|
52
|
+
set_theme: boolean
|
|
53
|
+
show_detected: boolean
|
|
54
|
+
show_os: boolean
|
|
55
|
+
show_providers: boolean
|
|
56
|
+
animations: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Api = Parameters<TuiPlugin>[0]
|
|
60
|
+
|
|
61
|
+
const rec = (value: unknown) => {
|
|
62
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
|
63
|
+
return Object.fromEntries(Object.entries(value))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const pick = (value: unknown, fallback: string) => {
|
|
67
|
+
if (typeof value !== "string") return fallback
|
|
68
|
+
if (!value.trim()) return fallback
|
|
69
|
+
return value
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const bool = (value: unknown, fallback: boolean) => {
|
|
73
|
+
if (typeof value !== "boolean") return fallback
|
|
74
|
+
return value
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cfg = (opts: Record<string, unknown> | undefined): Cfg => {
|
|
78
|
+
return {
|
|
79
|
+
enabled: bool(opts?.enabled, true),
|
|
80
|
+
theme: pick(opts?.theme, "gentleman"),
|
|
81
|
+
set_theme: bool(opts?.set_theme, true),
|
|
82
|
+
show_detected: bool(opts?.show_detected, true),
|
|
83
|
+
show_os: bool(opts?.show_os, true),
|
|
84
|
+
show_providers: bool(opts?.show_providers, true),
|
|
85
|
+
animations: bool(opts?.animations, true),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Helper to detect OS name
|
|
90
|
+
const getOSName = (): string => {
|
|
91
|
+
try {
|
|
92
|
+
const platform = typeof process !== "undefined" ? process.platform : "unknown"
|
|
93
|
+
switch (platform) {
|
|
94
|
+
case "linux":
|
|
95
|
+
try {
|
|
96
|
+
const osRelease = readFileSync("/etc/os-release", "utf8")
|
|
97
|
+
const match = osRelease.match(/^NAME="?([^"\n]+)"?/m)
|
|
98
|
+
if (match) return match[1]
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
return "Linux"
|
|
102
|
+
case "darwin":
|
|
103
|
+
return "macOS"
|
|
104
|
+
case "win32":
|
|
105
|
+
return "Windows"
|
|
106
|
+
default:
|
|
107
|
+
return platform
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
return "Unknown"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Map provider IDs to friendly display names
|
|
115
|
+
const providerDisplayNames: Record<string, string> = {
|
|
116
|
+
"openai": "OpenAI",
|
|
117
|
+
"google": "Google",
|
|
118
|
+
"github-copilot": "Copilot",
|
|
119
|
+
"opencode-go": "OpenCode GO",
|
|
120
|
+
"anthropic": "Claude",
|
|
121
|
+
"deepseek": "DeepSeek",
|
|
122
|
+
"openrouter": "OpenRouter",
|
|
123
|
+
"mistral": "Mistral",
|
|
124
|
+
"groq": "Groq",
|
|
125
|
+
"cohere": "Cohere",
|
|
126
|
+
"together": "Together",
|
|
127
|
+
"perplexity": "Perplexity",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Helper to detect active providers from OpenCode state
|
|
131
|
+
const getProviders = (providers: ReadonlyArray<{ id: string; name: string }> | undefined): string => {
|
|
132
|
+
if (!providers || providers.length === 0) {
|
|
133
|
+
return "No providers configured"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Map provider IDs to friendly names and deduplicate
|
|
137
|
+
const names = new Set<string>()
|
|
138
|
+
for (const provider of providers) {
|
|
139
|
+
const displayName = providerDisplayNames[provider.id] || provider.name || provider.id
|
|
140
|
+
names.add(displayName)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Return compact comma-separated list
|
|
144
|
+
return Array.from(names).sort().join(", ")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const Home = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?: boolean }) => {
|
|
148
|
+
const [eyeIndex, setEyeIndex] = createSignal(0)
|
|
149
|
+
const [showTongue, setShowTongue] = createSignal(false)
|
|
150
|
+
const [busyPhrase, setBusyPhrase] = createSignal("")
|
|
151
|
+
|
|
152
|
+
// Animation: subtle eye variation every 3-5 seconds
|
|
153
|
+
createEffect(() => {
|
|
154
|
+
if (!props.config.animations) return
|
|
155
|
+
|
|
156
|
+
const interval = setInterval(() => {
|
|
157
|
+
// Randomly change eyes occasionally (20% chance)
|
|
158
|
+
if (Math.random() < 0.2) {
|
|
159
|
+
setEyeIndex((Math.floor(Math.random() * eyeVariations.length)))
|
|
160
|
+
} else {
|
|
161
|
+
setEyeIndex(0) // back to neutral
|
|
162
|
+
}
|
|
163
|
+
}, 4000) // check every 4 seconds
|
|
164
|
+
|
|
165
|
+
onCleanup(() => clearInterval(interval))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Busy state animation: show tongue + rotate phrases
|
|
169
|
+
createEffect(() => {
|
|
170
|
+
if (!props.config.animations || !props.isBusy) {
|
|
171
|
+
setShowTongue(false)
|
|
172
|
+
setBusyPhrase("")
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setShowTongue(true)
|
|
177
|
+
|
|
178
|
+
let phraseIdx = 0
|
|
179
|
+
setBusyPhrase(busyPhrases[phraseIdx])
|
|
180
|
+
|
|
181
|
+
const interval = setInterval(() => {
|
|
182
|
+
phraseIdx = (phraseIdx + 1) % busyPhrases.length
|
|
183
|
+
setBusyPhrase(busyPhrases[phraseIdx])
|
|
184
|
+
}, 3000) // rotate every 3 seconds
|
|
185
|
+
|
|
186
|
+
onCleanup(() => clearInterval(interval))
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Build Mustachi with current eye state
|
|
190
|
+
const currentEyes = eyeVariations[eyeIndex()]
|
|
191
|
+
const mustachi = mustachiBase.map((line, idx) => {
|
|
192
|
+
// Replace eyes in line 2
|
|
193
|
+
if (idx === 2) {
|
|
194
|
+
return ` │ ${currentEyes.left} │ │ ${currentEyes.right} │ `
|
|
195
|
+
}
|
|
196
|
+
return line
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Add tongue if busy
|
|
200
|
+
if (showTongue()) {
|
|
201
|
+
mustachi.push(" ╲ ● ╱ ")
|
|
202
|
+
mustachi.push(" v ")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const topColor = props.theme.accent || "#E0C15A"
|
|
206
|
+
const midColor = props.theme.primary || "#7FB4CA"
|
|
207
|
+
const bottomColor = props.theme.error || "#CB7C94"
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<box flexDirection="column" alignItems="center">
|
|
211
|
+
{/* Mustachi with 3-tone gradient */}
|
|
212
|
+
{mustachi.map((line, idx) => {
|
|
213
|
+
const totalLines = mustachi.length
|
|
214
|
+
let color = midColor
|
|
215
|
+
if (idx < totalLines / 3) {
|
|
216
|
+
color = topColor
|
|
217
|
+
} else if (idx >= (2 * totalLines) / 3) {
|
|
218
|
+
color = bottomColor
|
|
219
|
+
}
|
|
220
|
+
return <text fg={color}>{line}</text>
|
|
221
|
+
})}
|
|
222
|
+
|
|
223
|
+
{/* OpenCode branding */}
|
|
224
|
+
<box flexDirection="row" gap={0}>
|
|
225
|
+
<text fg={props.theme.textMuted} dimColor={true}>╭</text>
|
|
226
|
+
<text fg={props.theme.primary} dimColor={false}> OpenCode </text>
|
|
227
|
+
<text fg={props.theme.textMuted} dimColor={true}>╮</text>
|
|
228
|
+
</box>
|
|
229
|
+
|
|
230
|
+
{/* Busy phrase if loading */}
|
|
231
|
+
{busyPhrase() && (
|
|
232
|
+
<text fg={props.theme.warning}>{busyPhrase()}</text>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
<text> </text>
|
|
236
|
+
</box>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const DetectedEnv = (props: {
|
|
241
|
+
theme: TuiThemeCurrent
|
|
242
|
+
providers: ReadonlyArray<{ id: string; name: string }> | undefined
|
|
243
|
+
config: Cfg
|
|
244
|
+
}) => {
|
|
245
|
+
if (!props.config.show_detected) return null
|
|
246
|
+
|
|
247
|
+
const os = props.config.show_os ? getOSName() : null
|
|
248
|
+
const providers = props.config.show_providers ? getProviders(props.providers) : null
|
|
249
|
+
|
|
250
|
+
// Don't render if nothing to show
|
|
251
|
+
if (!os && !providers) return null
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<box flexDirection="row" gap={1}>
|
|
255
|
+
<text fg={props.theme.textMuted}>Detected:</text>
|
|
256
|
+
{os && <text fg={props.theme.text}>{os}</text>}
|
|
257
|
+
{os && providers && <text fg={props.theme.textMuted}>•</text>}
|
|
258
|
+
{providers && <text fg={props.theme.text}>{providers}</text>}
|
|
259
|
+
</box>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const tui: TuiPlugin = async (api, options) => {
|
|
264
|
+
const boot = cfg(rec(options))
|
|
265
|
+
if (!boot.enabled) return
|
|
266
|
+
|
|
267
|
+
const [value] = createSignal(boot)
|
|
268
|
+
const [isBusy, setIsBusy] = createSignal(false)
|
|
269
|
+
|
|
270
|
+
await api.theme.install("./gentleman.json")
|
|
271
|
+
if (value().set_theme) {
|
|
272
|
+
api.theme.set(value().theme)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Detect busy state if API exposes it
|
|
276
|
+
// This is a best-effort detection - OpenCode TUI may or may not expose this
|
|
277
|
+
createEffect(() => {
|
|
278
|
+
try {
|
|
279
|
+
// Check if there's a running agent or session state
|
|
280
|
+
const hasRunningSession = api.state?.session?.running
|
|
281
|
+
setIsBusy(!!hasRunningSession)
|
|
282
|
+
} catch {
|
|
283
|
+
// If API doesn't expose this, animations will just use idle state
|
|
284
|
+
setIsBusy(false)
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
api.slots.register({
|
|
289
|
+
slots: {
|
|
290
|
+
home_logo(ctx) {
|
|
291
|
+
return <Home theme={ctx.theme.current} config={value()} isBusy={isBusy()} />
|
|
292
|
+
},
|
|
293
|
+
home_prompt_after(ctx) {
|
|
294
|
+
return <DetectedEnv theme={ctx.theme.current} providers={api.state.provider} config={value()} />
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
301
|
+
id,
|
|
302
|
+
tui,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default plugin
|