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.
Files changed (4) hide show
  1. package/README.md +315 -0
  2. package/gentleman.json +55 -0
  3. package/package.json +45 -0
  4. 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