plugin-gentleman 1.0.0 → 1.0.2

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.
Files changed (3) hide show
  1. package/README.md +158 -126
  2. package/package.json +1 -1
  3. package/tui.tsx +254 -63
package/README.md CHANGED
@@ -1,27 +1,38 @@
1
- # plugin-gentleman
1
+ # Plugin Gentleman
2
2
 
3
- OpenCode TUI plugin featuring **Mustachi** — an animated ASCII mascot with eyes, mustache, and optional motivational phrases during busy states.
3
+ > **Mustachi** — An animated ASCII mascot bringing personality to your OpenCode terminal.
4
4
 
5
- ## What This Is
5
+ An OpenCode TUI plugin that adds visual flair and environment awareness to your coding sessions:
6
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`
7
+ - 🎭 **Prominent ASCII mustache** on the home screen
8
+ - 👤 **Full Mustachi face** with eyes in the sidebar
9
+ - 👀 **Subtle eye animations** (optional, low-frequency)
10
+ - 💬 **Motivational phrases** during busy states (Rioplatense Spanish style)
11
+ - 🎨 **Gentleman theme** installed and applied automatically
12
+ - 🖥️ **Environment detection** displays your OS and LLM providers
13
+ - ⚙️ **Fully configurable** via `opencode.json`
14
14
 
15
15
  ## Installation
16
16
 
17
- ### Method 1: Local Testing with Tarball (Recommended First)
17
+ ### Quick Start
18
+
19
+ Install the plugin globally with OpenCode:
18
20
 
19
21
  ```bash
20
- npm pack
21
- npm install -g ./plugin-gentleman-1.0.0.tgz
22
+ opencode plugin plugin-gentleman --global
22
23
  ```
23
24
 
24
- Add to `~/.config/opencode/opencode.json`:
25
+ OpenCode will download the package and update your TUI config automatically.
26
+
27
+ **Restart OpenCode** to see:
28
+ - Prominent ASCII mustache on the home screen
29
+ - `OpenCode` branding text
30
+ - `Detected: <OS> · <providers>` below the prompt area
31
+ - Full Mustachi face in the sidebar
32
+
33
+ ### Alternative: Manual Configuration
34
+
35
+ If you prefer managing config yourself, add this to `~/.config/opencode/opencode.json`:
25
36
 
26
37
  ```json
27
38
  {
@@ -30,19 +41,19 @@ Add to `~/.config/opencode/opencode.json`:
30
41
  }
31
42
  ```
32
43
 
33
- Restart OpenCode:
44
+ OpenCode will install npm plugins automatically on startup.
34
45
 
35
- ```bash
36
- opencode
37
- ```
46
+ ---
47
+
48
+ ### For Developers
38
49
 
39
- ### Method 2: Development with npm link
50
+ **Local Testing with npm link:**
40
51
 
41
52
  ```bash
42
53
  npm link
43
54
  ```
44
55
 
45
- Add to `~/.config/opencode/opencode.json`:
56
+ Then add to `~/.config/opencode/opencode.json`:
46
57
 
47
58
  ```json
48
59
  {
@@ -51,78 +62,95 @@ Add to `~/.config/opencode/opencode.json`:
51
62
  }
52
63
  ```
53
64
 
54
- Restart OpenCode.
65
+ Restart OpenCode to see changes.
55
66
 
56
- ### Method 3: npm Registry (After Publishing)
67
+ **Local Testing with Tarball:**
57
68
 
58
69
  ```bash
59
- npm install -g plugin-gentleman
60
- opencode plugin install plugin-gentleman
70
+ npm pack
71
+ opencode plugin ./plugin-gentleman-<version>.tgz --global
61
72
  ```
62
73
 
74
+ *If your OpenCode version doesn't accept a tarball path, use the published package flow instead.*
75
+
76
+ ---
77
+
63
78
  ## Features
64
79
 
65
- ### Mustachi Mascot
80
+ ### Visual Components
81
+
82
+ **Home Screen:**
83
+ - Prominent ASCII mustache (no face, just the mustache)
84
+ - "OpenCode" branding text
85
+ - Environment detection showing `OS · Providers` below the prompt area
66
86
 
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
87
+ **Sidebar:**
88
+ - Full Mustachi face with eyes and mustache
89
+ - Animated eyes that occasionally look around *(when animations enabled)*
90
+ - Tongue and motivational phrases during busy states *(when animations enabled)*
72
91
 
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..."
92
+ ### The Mustachi Mascot
80
93
 
81
- ### Animation Behavior
94
+ An ASCII character with personality:
95
+ - **Eyes** that occasionally look in different directions *(sidebar only)*
96
+ - **Mustache** rendered in theme colors *(both home and sidebar)*
97
+ - **Tongue** that appears during busy/loading states *(sidebar only)*
98
+ - **Motivational phrases** in Rioplatense Spanish style *(sidebar only)*
82
99
 
83
- **Low complexity, low frequency** — designed to be subtle and non-intrusive:
100
+ **Example phrases during busy states:**
101
+ - *"Ponete las pilas, hermano..."*
102
+ - *"Dale que va, dale que va..."*
103
+ - *"Ya casi, ya casi..."*
104
+ - *"Ahí vamos, loco..."*
105
+ - *"Un toque más y listo..."*
106
+ - *"Aguantá que estoy pensando..."*
84
107
 
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
108
+ ### Animations
89
109
 
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
110
+ **Low complexity, low frequency** subtle and non-intrusive:
94
111
 
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
112
+ **Eye Direction** *(when `animations: true`)*
113
+ - Every ~4 seconds, eyes may look in a random direction
114
+ - 20% chance of movement per interval
115
+ - Returns to neutral position most of the time
99
116
 
100
- ### Theme
117
+ **Busy State** *(when `animations: true`)*
118
+ - Tongue appears when OpenCode is processing
119
+ - Motivational phrase rotates every 3 seconds
120
+ - Only active during detected busy states
101
121
 
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`)
122
+ **Disabled** *(when `animations: false`)*
123
+ - Mustachi stays in neutral position
124
+ - No eye movement, tongue, or phrases
107
125
 
108
- Mustachi uses a 3-tone gradient:
109
- - **Top**: Accent (gold)
110
- - **Middle**: Primary (blue)
111
- - **Bottom**: Error/pink (`#CB7C94`)
126
+ ### Gentleman Theme
127
+
128
+ A refined dark color palette:
129
+ - **Background:** Deep navy (`#06080f`)
130
+ - **Primary:** Soft blue (`#7FB4CA`)
131
+ - **Accent:** Warm gold (`#E0C15A`)
132
+ - **Text:** Clean white (`#F3F6F9`)
133
+
134
+ **Mustachi gradient** (3-tone):
135
+ - **Top:** Accent gold
136
+ - **Middle:** Primary blue
137
+ - **Bottom:** Error pink (`#CB7C94`)
112
138
 
113
139
  ### Environment Detection
114
140
 
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.)
141
+ Displays a "Detected" line with:
142
+ - **OS Detection:** Reads distro name on Linux, shows "macOS" or "Windows" on other platforms
143
+ - **Provider Detection:** Lists active LLM providers (OpenAI, Copilot, Google, etc.)
118
144
 
119
145
  Both are fully configurable and can be hidden.
120
146
 
147
+ ---
148
+
121
149
  ## Configuration
122
150
 
123
- All options are set via plugin tuple syntax in `opencode.json`:
151
+ All options are configured via plugin tuple syntax in `opencode.json`.
124
152
 
125
- ### Default Configuration
153
+ ### Default Settings
126
154
 
127
155
  ```json
128
156
  {
@@ -144,7 +172,7 @@ All options are set via plugin tuple syntax in `opencode.json`:
144
172
  }
145
173
  ```
146
174
 
147
- ### Configuration Options
175
+ ### Available Options
148
176
 
149
177
  | Option | Type | Default | Description |
150
178
  |--------|------|---------|-------------|
@@ -156,7 +184,9 @@ All options are set via plugin tuple syntax in `opencode.json`:
156
184
  | `show_providers` | boolean | `true` | Show detected LLM providers |
157
185
  | `animations` | boolean | `true` | Enable Mustachi animations (eyes, busy state) |
158
186
 
159
- ### Example: Disable Animations
187
+ ### Examples
188
+
189
+ **Disable Animations:**
160
190
 
161
191
  ```json
162
192
  {
@@ -169,7 +199,7 @@ All options are set via plugin tuple syntax in `opencode.json`:
169
199
 
170
200
  Mustachi will remain static with neutral eyes and no busy-state expressions.
171
201
 
172
- ### Example: Logo Only (No Detection)
202
+ **Logo Only (No Detection):**
173
203
 
174
204
  ```json
175
205
  {
@@ -180,9 +210,9 @@ Mustachi will remain static with neutral eyes and no busy-state expressions.
180
210
  }
181
211
  ```
182
212
 
183
- Shows only Mustachi and the OpenCode branding, no OS/provider info.
213
+ Shows only Mustachi and OpenCode branding, no OS/provider info.
184
214
 
185
- ### Example: Show Only OS
215
+ **Show Only OS:**
186
216
 
187
217
  ```json
188
218
  {
@@ -200,25 +230,32 @@ Shows only Mustachi and the OpenCode branding, no OS/provider info.
200
230
  }
201
231
  ```
202
232
 
233
+ ---
234
+
203
235
  ## How It Works
204
236
 
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)
237
+ The plugin integrates with OpenCode's TUI system through slot registration:
238
+
239
+ 1. **Theme Installation:** Installs `gentleman.json` into OpenCode themes on load
240
+ 2. **Theme Activation:** Switches to the gentleman theme *(if `set_theme: true`)*
241
+ 3. **Home Logo Slot:** Registers `home_logo` with mustache-only ASCII art
242
+ 4. **Environment Detection Slot:** Registers `home_bottom` with OS/provider info below the prompt
243
+ 5. **Sidebar Slot:** Registers `sidebar_content` with full Mustachi face and animations
244
+ 6. **Animation Loop:** Starts interval timers for eye variations and busy-state detection *(if `animations: true`, sidebar only)*
245
+ 7. **Busy State Detection:** Attempts to read `api.state.session.running` *(best-effort; may not be exposed by all OpenCode versions)*
246
+
247
+ ### Technical Stack
211
248
 
212
- ### Technical Details
249
+ - **No Build Step:** Plain TSX transpiled at runtime by OpenCode
250
+ - **Solid.js Reactivity:** Uses `createSignal` and `createEffect` for animations
251
+ - **Safe Detection:** All OS/provider detection wrapped in try-catch blocks
252
+ - **Cleanup:** Uses `onCleanup` to clear intervals when component unmounts
213
253
 
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
254
+ ---
218
255
 
219
256
  ## Supported Providers
220
257
 
221
- The plugin maps these provider IDs to friendly names:
258
+ Friendly name mapping for LLM providers:
222
259
 
223
260
  | Provider ID | Display Name |
224
261
  |-------------|--------------|
@@ -235,80 +272,75 @@ The plugin maps these provider IDs to friendly names:
235
272
  | `together` | Together |
236
273
  | `perplexity` | Perplexity |
237
274
 
238
- Unknown provider IDs display the configured name or raw ID.
275
+ *Unknown provider IDs display the configured name or raw ID.*
239
276
 
240
- ## Local Testing Limitations
277
+ ---
241
278
 
242
- **IMPORTANT**: This is a **TUI plugin** for npm installation.
279
+ ## Important Notes
243
280
 
244
- If you try to use this as a local system plugin (copying `.ts` files to `~/.config/opencode/plugins/`):
281
+ ### TUI Plugin vs System Plugin
282
+
283
+ **This is a TUI plugin for npm installation.**
284
+
285
+ If you copy `.ts` files to `~/.config/opencode/plugins/` (system plugin):
245
286
  - ❌ **NO visual changes** — system plugins cannot modify the TUI
246
- - ❌ **NO Mustachi** — only TUI plugins can register slot components
287
+ - ❌ **NO Mustachi** — only TUI plugins can register slot components
247
288
  - ❌ **NO animations** — JSX/Solid.js components only work in TUI plugins
248
289
 
249
- **For full features, use npm installation** (tarball or link method above).
290
+ **For full features, use npm installation** (recommended global method above).
250
291
 
251
- ## Repository Structure
292
+ ### Package Contents
252
293
 
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):
294
+ **Files included in npm package** *(via `files` field in `package.json`)*:
266
295
  - `tui.tsx` — main plugin implementation
267
296
  - `gentleman.json` — theme definition
268
297
  - `package.json` — auto-included by npm
269
298
  - `README.md` — auto-included by npm
270
299
 
271
- **Local-only files** (excluded from npm, kept in repo for development/reference):
300
+ **Repository-only files** *(excluded from npm package)*:
272
301
  - `gentleman-local.ts` — legacy local system plugin with limited features
273
302
  - `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
303
 
285
- ## Caveats Before Publishing
304
+ ---
305
+
306
+ ## Known Limitations
307
+
308
+ 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 in sidebar)*.
286
309
 
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).
310
+ 2. **Animation Frequency:** Currently set to low frequency (4s for eyes, 3s for phrase rotation). Adjust the intervals in `tui.tsx` if needed.
288
311
 
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`.
312
+ 3. **Slot Usage:** The plugin uses these OpenCode TUI slots:
313
+ - `home_logo` — mustache-only ASCII art
314
+ - `home_bottom` — environment detection (OS + providers)
315
+ - `sidebar_content` — full Mustachi face with animations
290
316
 
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.
317
+ 4. **Theme Compatibility:** The plugin installs and optionally activates the Gentleman theme. If you prefer a custom theme, set `set_theme: false`.
292
318
 
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`.
319
+ ---
294
320
 
295
321
  ## Development
296
322
 
297
- To modify the plugin:
323
+ ### Modifying the Plugin
324
+
325
+ To work on the plugin:
298
326
 
299
327
  1. Edit `tui.tsx` (main implementation)
300
328
  2. Test locally with `npm link` or `npm pack`
301
329
  3. No build step needed — OpenCode transpiles TSX at runtime
302
330
  4. Restart OpenCode to see changes
303
331
 
304
- To add new eye variations:
305
- - Edit the `eyeVariations` array in `tui.tsx`
332
+ ### Adding New Content
333
+
334
+ **Eye variations:**
335
+ - Edit the `eyeVariations` array in `tui.tsx` *(affects sidebar only)*
336
+
337
+ **Busy phrases:**
338
+ - Edit the `busyPhrases` array in `tui.tsx` *(affects sidebar only)*
306
339
 
307
- To add new busy phrases:
308
- - Edit the `busyPhrases` array in `tui.tsx`
340
+ **Animation timings:**
341
+ - Adjust `setInterval` durations in the `SidebarMustachi` component
309
342
 
310
- To change animation timings:
311
- - Adjust `setInterval` durations in the `Home` component
343
+ ---
312
344
 
313
345
  ## License
314
346
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "plugin-gentleman",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "description": "OpenCode TUI plugin featuring Mustachi - an animated ASCII mascot with eyes, mustache, and optional motivational phrases during busy states",
6
6
  "type": "module",
7
7
  "exports": {
package/tui.tsx CHANGED
@@ -6,32 +6,123 @@ import { createSignal, onCleanup, createEffect } from "solid-js"
6
6
 
7
7
  const id = "gentleman"
8
8
 
9
- // Mustachi ASCII art - inspired by mustachi examples (eyes, mustache, optional tongue)
10
- // Base state: neutral look
11
- const mustachiBase = [
9
+ // Premium Mustachi ASCII art - full version for sidebar
10
+ // Base structure with eyes that will be replaced dynamically
11
+ const mustachiNeutralBase = [
12
+ " ████████████ ████████████",
13
+ " ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
14
+ " ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
15
+ " ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
16
+ " ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
17
+ " ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
18
+ " ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
19
+ " ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
20
+ " ████████████ ████████████",
21
+ ]
22
+
23
+ // Squinted eyes version for busy state
24
+ const mustachiSquintedBase = [
25
+ " ████████████ ████████████",
26
+ " ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
27
+ " ██░░░████████░░░██ ██░░░░░░░░░░░░░░██",
28
+ " ██░░████░░░░████░░██ ██░░░░░░░░░░░░░░░░██",
29
+ " ██░░██░░░░░░░░██░░██ ██░░░░░░░░░░░░░░░░██",
30
+ " ██░░░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░░░██",
31
+ " ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
32
+ " ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
33
+ " ████████████ ████████████",
34
+ ]
35
+
36
+ // Mustache section (shared by all states)
37
+ const mustachiMustacheSection = [
38
+ " ████████ ████████",
39
+ " ████████████ ████████████",
40
+ " ██ ████████████████ ████████████████ ██",
41
+ " ████ ████████████████████ ████████████████████ ████",
42
+ " ██████ ███████████████████████████████████████████ ██████",
43
+ " ███████████████████████████████████████████████████████████",
44
+ " ███████████████████████████████████████████████████████████",
45
+ " ███████████████████████████████████████████████████████████",
46
+ " █████████████████████████████████████████████████████████",
47
+ " ███████████████████████████████████████████████████████",
48
+ " ▓▓█████████████████████ █████████████████████▓▓",
49
+ " ▓▓▓███████████████ ███████████████▓▓▓",
50
+ " ▓▓▓█████████ █████████▓▓▓",
51
+ " ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓",
52
+ ]
53
+
54
+ // Tongue animation frames (progressive)
55
+ const tongueFrames = [
56
+ [], // no tongue
57
+ [" ███████"], // small tongue
58
+ [" ███████", " █████"], // medium tongue
59
+ [" ███████", " █████", " ███"], // full tongue
60
+ ]
61
+
62
+ // Mustache-only ASCII art for home logo (prominent and simple)
63
+ const mustachiMustacheOnly = [
12
64
  "",
13
- " ╭─────╮ ╭─────╮ ",
14
- " │ ● ● │ │ ○ ○ │ ",
15
- " ╰─────╯ ╰─────╯ ",
16
- " ╲ ╱ ",
17
- " ╲ ╱ ",
18
- " ╭─────═════════─────╮ ",
19
- " ╱ ╭───────────╮ ╲ ",
20
- " ╱ │ ~~~~~~~~~ │ ╲ ",
21
- "╱ ╰───────────╯ ╲ ",
22
- "╲ ╱ ",
23
- " ╲ ╱ ",
24
- " ╰───────────────────╯ ",
65
+ " ████████ ████████",
66
+ " ████████████ ████████████",
67
+ " ██ ████████████████ ████████████████ ██",
68
+ " ████ ████████████████████ ████████████████████ ████",
69
+ " ██████ ███████████████████████████████████████████ ██████",
70
+ " ███████████████████████████████████████████████████████████",
71
+ " ███████████████████████████████████████████████████████████",
72
+ " ███████████████████████████████████████████████████████████",
73
+ " █████████████████████████████████████████████████████████",
74
+ " ███████████████████████████████████████████████████████",
75
+ " ▓▓█████████████████████ █████████████████████▓▓",
76
+ " ▓▓▓███████████████ ███████████████▓▓▓",
77
+ " ▓▓▓█████████ █████████▓▓▓",
78
+ " ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓",
25
79
  "",
26
80
  ]
27
81
 
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
82
+ // Left pupil positions for look-around animation (progressive)
83
+ // Modifies only the left eye (white sclera with dark pupil)
84
+ // Right eye is monocle/glass and remains static
85
+ const leftPupilPositions = [
86
+ "████████", // center (line 3 of eyes)
87
+ "██████ ", // looking left
88
+ " ██████", // looking right
89
+ "████████", // center again
90
+ ]
91
+
92
+ // Blink animation frames (progressive) - affects both eyes
93
+ const blinkFrames = [
94
+ // Open eyes (default state embedded in base arrays)
95
+ { left: mustachiNeutralBase, squinted: mustachiSquintedBase },
96
+ // Half closed
97
+ {
98
+ left: [
99
+ " ████████████ ████████████",
100
+ " ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
101
+ " ██░░░░████░░░░░░██ ██░░░░░░░░░░░░░░██",
102
+ " ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
103
+ " ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
104
+ " ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
105
+ " ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
106
+ " ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
107
+ " ████████████ ████████████",
108
+ ],
109
+ squinted: mustachiSquintedBase // squinted stays squinted during blink
110
+ },
111
+ // Fully closed
112
+ {
113
+ left: [
114
+ " ████████████ ████████████",
115
+ " ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
116
+ " ██░░████████░░░░██ ██░░░░░░░░░░░░░░██",
117
+ " ██░░████████████░░██ ██░░░░░░░░░░░░░░░░██",
118
+ " ██░░████████████░░██ ██░░░░░░░░░░░░░░░░██",
119
+ " ██░░████████████░░██ ██░░░░░░░░░░░░░░░░██",
120
+ " ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
121
+ " ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
122
+ " ████████████ ████████████",
123
+ ],
124
+ squinted: mustachiSquintedBase
125
+ },
35
126
  ]
36
127
 
37
128
  // Busy/loading state with tongue and motivational phrases
@@ -144,73 +235,177 @@ const getProviders = (providers: ReadonlyArray<{ id: string; name: string }> | u
144
235
  return Array.from(names).sort().join(", ")
145
236
  }
146
237
 
147
- const Home = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?: boolean }) => {
148
- const [eyeIndex, setEyeIndex] = createSignal(0)
149
- const [showTongue, setShowTongue] = createSignal(false)
238
+ // Home logo: Mustache-only (simple and prominent)
239
+ const HomeLogo = (props: { theme: TuiThemeCurrent }) => {
240
+ const topColor = props.theme.accent || "#E0C15A"
241
+ const midColor = props.theme.primary || "#7FB4CA"
242
+ const bottomColor = props.theme.error || "#CB7C94"
243
+
244
+ return (
245
+ <box flexDirection="column" alignItems="center">
246
+ {/* Mustache-only with 3-tone gradient */}
247
+ {mustachiMustacheOnly.map((line, idx) => {
248
+ const totalLines = mustachiMustacheOnly.length
249
+ let color = midColor
250
+ if (idx < totalLines / 3) {
251
+ color = topColor
252
+ } else if (idx >= (2 * totalLines) / 3) {
253
+ color = bottomColor
254
+ }
255
+ return <text fg={color}>{line}</text>
256
+ })}
257
+
258
+ {/* OpenCode branding */}
259
+ <box flexDirection="row" gap={0}>
260
+ <text fg={props.theme.textMuted} dimColor={true}>╭</text>
261
+ <text fg={props.theme.primary} dimColor={false}> OpenCode </text>
262
+ <text fg={props.theme.textMuted} dimColor={true}>╮</text>
263
+ </box>
264
+
265
+ <text> </text>
266
+ </box>
267
+ )
268
+ }
269
+
270
+ // Sidebar: Full Mustachi face with progressive animations
271
+ const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?: boolean }) => {
272
+ const [pupilIndex, setPupilIndex] = createSignal(0)
273
+ const [blinkFrame, setBlinkFrame] = createSignal(0)
274
+ const [tongueFrame, setTongueFrame] = createSignal(0)
150
275
  const [busyPhrase, setBusyPhrase] = createSignal("")
151
276
 
152
- // Animation: subtle eye variation every 3-5 seconds
277
+ // Animation: pupil movement (look around) - low frequency, progressive
278
+ createEffect(() => {
279
+ if (!props.config.animations || props.isBusy) {
280
+ setPupilIndex(0)
281
+ return
282
+ }
283
+
284
+ const interval = setInterval(() => {
285
+ // Cycle through pupil positions progressively
286
+ setPupilIndex((prev) => {
287
+ // 80% chance to stay at center, 20% to move
288
+ if (Math.random() < 0.8) return 0
289
+ return (prev + 1) % leftPupilPositions.length
290
+ })
291
+ }, 3000)
292
+
293
+ onCleanup(() => clearInterval(interval))
294
+ })
295
+
296
+ // Animation: blink - occasional, progressive
153
297
  createEffect(() => {
154
298
  if (!props.config.animations) return
155
299
 
300
+ const blinkSequence = async () => {
301
+ // Open -> half -> closed -> half -> open
302
+ setBlinkFrame(0)
303
+ await new Promise(r => setTimeout(r, 150))
304
+ setBlinkFrame(1)
305
+ await new Promise(r => setTimeout(r, 100))
306
+ setBlinkFrame(2)
307
+ await new Promise(r => setTimeout(r, 100))
308
+ setBlinkFrame(1)
309
+ await new Promise(r => setTimeout(r, 100))
310
+ setBlinkFrame(0)
311
+ }
312
+
156
313
  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
314
+ // Blink occasionally (15% chance every 4s)
315
+ if (Math.random() < 0.15) {
316
+ blinkSequence()
162
317
  }
163
- }, 4000) // check every 4 seconds
318
+ }, 4000)
164
319
 
165
320
  onCleanup(() => clearInterval(interval))
166
321
  })
167
322
 
168
- // Busy state animation: show tongue + rotate phrases
323
+ // Busy state animation: tongue grows progressively + rotate phrases
169
324
  createEffect(() => {
170
325
  if (!props.config.animations || !props.isBusy) {
171
- setShowTongue(false)
326
+ setTongueFrame(0)
172
327
  setBusyPhrase("")
173
328
  return
174
329
  }
175
330
 
176
- setShowTongue(true)
331
+ // Grow tongue progressively when entering busy state
332
+ let currentFrame = 0
333
+ let tongueTimeoutId: NodeJS.Timeout | undefined
334
+ const growTongue = () => {
335
+ if (currentFrame < tongueFrames.length - 1) {
336
+ currentFrame++
337
+ setTongueFrame(currentFrame)
338
+ tongueTimeoutId = setTimeout(growTongue, 200)
339
+ }
340
+ }
341
+ growTongue()
177
342
 
343
+ // Rotate busy phrases
178
344
  let phraseIdx = 0
179
345
  setBusyPhrase(busyPhrases[phraseIdx])
180
346
 
181
347
  const interval = setInterval(() => {
182
348
  phraseIdx = (phraseIdx + 1) % busyPhrases.length
183
349
  setBusyPhrase(busyPhrases[phraseIdx])
184
- }, 3000) // rotate every 3 seconds
350
+ }, 3000)
185
351
 
186
- onCleanup(() => clearInterval(interval))
352
+ onCleanup(() => {
353
+ clearInterval(interval)
354
+ if (tongueTimeoutId !== undefined) {
355
+ clearTimeout(tongueTimeoutId)
356
+ }
357
+ })
187
358
  })
188
359
 
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} │ `
360
+ // Build the complete Mustachi face
361
+ const buildFace = () => {
362
+ const lines: string[] = []
363
+
364
+ // Select eye base based on busy state
365
+ let eyeBase = props.isBusy ? mustachiSquintedBase : mustachiNeutralBase
366
+
367
+ // Apply blink animation if active
368
+ if (blinkFrame() > 0 && blinkFrame() < blinkFrames.length) {
369
+ eyeBase = props.isBusy
370
+ ? blinkFrames[blinkFrame()].squinted
371
+ : blinkFrames[blinkFrame()].left
195
372
  }
196
- return line
197
- })
198
-
199
- // Add tongue if busy
200
- if (showTongue()) {
201
- mustachi.push(" ╲ ╱ ")
202
- mustachi.push(" v ")
373
+
374
+ // Add eyes with pupil position (modify line 3 for left eye pupil)
375
+ eyeBase.forEach((line, idx) => {
376
+ if (idx === 3 && !props.isBusy && pupilIndex() >= 0) {
377
+ // Replace pupil in left eye (center of line 3)
378
+ const pupil = leftPupilPositions[pupilIndex()]
379
+ const modifiedLine = line.substring(0, 14) + pupil + line.substring(22)
380
+ lines.push(modifiedLine)
381
+ } else {
382
+ lines.push(line)
383
+ }
384
+ })
385
+
386
+ // Add mustache section
387
+ mustachiMustacheSection.forEach(line => lines.push(line))
388
+
389
+ // Add tongue if busy (progressive frames)
390
+ if (props.isBusy && tongueFrame() > 0) {
391
+ const tongueLines = tongueFrames[tongueFrame()]
392
+ tongueLines.forEach(line => lines.push(line))
393
+ }
394
+
395
+ return lines
203
396
  }
204
397
 
398
+ const faceLines = buildFace()
399
+
205
400
  const topColor = props.theme.accent || "#E0C15A"
206
401
  const midColor = props.theme.primary || "#7FB4CA"
207
402
  const bottomColor = props.theme.error || "#CB7C94"
208
403
 
209
404
  return (
210
405
  <box flexDirection="column" alignItems="center">
211
- {/* Mustachi with 3-tone gradient */}
212
- {mustachi.map((line, idx) => {
213
- const totalLines = mustachi.length
406
+ {/* Full Mustachi face with 3-tone gradient */}
407
+ {faceLines.map((line, idx) => {
408
+ const totalLines = faceLines.length
214
409
  let color = midColor
215
410
  if (idx < totalLines / 3) {
216
411
  color = topColor
@@ -220,13 +415,6 @@ const Home = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?: boolean })
220
415
  return <text fg={color}>{line}</text>
221
416
  })}
222
417
 
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
418
  {/* Busy phrase if loading */}
231
419
  {busyPhrase() && (
232
420
  <text fg={props.theme.warning}>{busyPhrase()}</text>
@@ -254,7 +442,7 @@ const DetectedEnv = (props: {
254
442
  <box flexDirection="row" gap={1}>
255
443
  <text fg={props.theme.textMuted}>Detected:</text>
256
444
  {os && <text fg={props.theme.text}>{os}</text>}
257
- {os && providers && <text fg={props.theme.textMuted}>•</text>}
445
+ {os && providers && <text fg={props.theme.textMuted}>·</text>}
258
446
  {providers && <text fg={props.theme.text}>{providers}</text>}
259
447
  </box>
260
448
  )
@@ -288,11 +476,14 @@ const tui: TuiPlugin = async (api, options) => {
288
476
  api.slots.register({
289
477
  slots: {
290
478
  home_logo(ctx) {
291
- return <Home theme={ctx.theme.current} config={value()} isBusy={isBusy()} />
479
+ return <HomeLogo theme={ctx.theme.current} />
292
480
  },
293
- home_prompt_after(ctx) {
481
+ home_bottom(ctx) {
294
482
  return <DetectedEnv theme={ctx.theme.current} providers={api.state.provider} config={value()} />
295
483
  },
484
+ sidebar_content(ctx) {
485
+ return <SidebarMustachi theme={ctx.theme.current} config={value()} isBusy={isBusy()} />
486
+ },
296
487
  },
297
488
  })
298
489
  }