opencode-buddy 0.3.4 → 0.3.7
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 +52 -32
- package/package.json +1 -1
- package/src/species.js +87 -81
- package/src/tui-plugin.jsx +41 -11
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ A virtual ASCII pet companion that lives in the opencode TUI sidebar. Hatches, f
|
|
|
24
24
|
└──────────────────────────┘
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The buddy
|
|
27
|
+
The buddy renders inside the TUI sidebar, reacts to your coding sessions (cheering when a turn finishes, getting scared on errors, falling asleep when low on energy), and can be controlled via slash commands typed directly in the prompt.
|
|
28
28
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
@@ -34,14 +34,12 @@ Requires opencode ≥ 1.15.
|
|
|
34
34
|
npm install -g opencode-buddy
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
The `postinstall` script automatically registers the plugin in both config files using the same spec. opencode picks the right entrypoint from the package's `exports` field:
|
|
37
|
+
The `postinstall` script automatically registers the plugin in both config files using the same spec. opencode picks the right entrypoint from the package's `exports` field based on runtime kind:
|
|
38
38
|
|
|
39
|
-
- `opencode
|
|
40
|
-
- `
|
|
39
|
+
- `opencode.json` (kind: `server`) → `src/server-plugin.js` via `main` (no-op since 0.3.x)
|
|
40
|
+
- `tui.json` (kind: `tui`) → `src/tui-plugin.jsx` via `exports["./tui"]`
|
|
41
41
|
|
|
42
|
-
Both `~/.config/opencode/opencode.json` and `~/.config/opencode/tui.json` get the same `plugin: ["opencode-buddy"]` entry.
|
|
43
|
-
|
|
44
|
-
If you have those config files as JSONC (with comments), the script skips them — add the entries manually:
|
|
42
|
+
Both `~/.config/opencode/opencode.json` and `~/.config/opencode/tui.json` get the same `plugin: ["opencode-buddy"]` entry. The `postinstall` is JSONC-safe — it leaves any file that contains comments alone and asks you to add the entry manually:
|
|
45
43
|
|
|
46
44
|
```jsonc
|
|
47
45
|
// ~/.config/opencode/opencode.json
|
|
@@ -50,7 +48,7 @@ If you have those config files as JSONC (with comments), the script skips them
|
|
|
50
48
|
}
|
|
51
49
|
```
|
|
52
50
|
|
|
53
|
-
```
|
|
51
|
+
```jsonc
|
|
54
52
|
// ~/.config/opencode/tui.json
|
|
55
53
|
{
|
|
56
54
|
"$schema": "https://opencode.ai/tui.json",
|
|
@@ -75,22 +73,37 @@ Once installed, type `/` in the prompt to see the slash commands. The buddy ship
|
|
|
75
73
|
|
|
76
74
|
The buddy also reacts passively to your sessions:
|
|
77
75
|
|
|
78
|
-
- `session.idle` → 4 second **celebrating**
|
|
79
|
-
- `session.error` → 5 second **scared**
|
|
80
|
-
- Energy < 20 → automatically transitions to **sleeping**
|
|
76
|
+
- `session.idle` → 4 second **celebrating** state
|
|
77
|
+
- `session.error` → 5 second **scared** state
|
|
78
|
+
- Energy < 20 → automatically transitions to **sleeping** (animation pauses)
|
|
81
79
|
- Hunger < 25 → transitions to **scared** for 30 seconds
|
|
82
80
|
|
|
83
81
|
State is shared across all opencode sessions via `~/.config/opencode-buddy/state.json`. Open a session in a different terminal and your buddy is still there.
|
|
84
82
|
|
|
83
|
+
### What you see in the sidebar
|
|
84
|
+
|
|
85
|
+
The sidebar shows the buddy's species-specific ASCII art, plus four lines under it:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
[BDDY] Quack the cat <- species + name (preceded by a rotating status dot)
|
|
89
|
+
<art rows> <- 6 rows of the species art
|
|
90
|
+
hunger |████░░░░░░| 79 <- hunger bar
|
|
91
|
+
happy |███░░░░░░░| 60 <- happiness bar
|
|
92
|
+
energy |███████░░░| 70 <- energy bar
|
|
93
|
+
idle Lv1 xp5/50 <- state label + level + xp progress
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The bottom state label reflects the persisted `state` field in `state.json`. It changes on `session.idle` (4 s "celebrating"), `session.error` (5 s "scared"), and on energy/hunger thresholds (auto "sleeping" or "scared"). The art's idle state has 3 frames; the sidebar re-renders the current frame each time `state.json` is reloaded.
|
|
97
|
+
|
|
85
98
|
## Six species
|
|
86
99
|
|
|
87
100
|
```
|
|
88
101
|
duck cat dragon
|
|
89
|
-
__ /\_/\
|
|
90
|
-
<(o )___ ( o.o ) (
|
|
91
|
-
( ._> / > ^ < >
|
|
92
|
-
`--' /| |\ /|
|
|
93
|
-
~ idle ~ (_| |_) (_|
|
|
102
|
+
__ /\_/\ /^^\
|
|
103
|
+
<(o )___ ( o.o ) (o o) ~~
|
|
104
|
+
( ._> / > ^ < >w< ~
|
|
105
|
+
`--' /| |\ /| |\
|
|
106
|
+
~ idle ~ (_| |_) (_| |_)
|
|
94
107
|
meow rawr
|
|
95
108
|
|
|
96
109
|
axolotl robot ghost
|
|
@@ -102,7 +115,7 @@ State is shared across all opencode sessions via `~/.config/opencode-buddy/state
|
|
|
102
115
|
~ ambien beep boo
|
|
103
116
|
```
|
|
104
117
|
|
|
105
|
-
Each species has a per-character color palette. The idle state is a
|
|
118
|
+
Each species has a per-character color palette. The idle state has 3 frames; `frameCount` is exposed so the View can pick a frame to render. The buddy renders whatever frame `frame() % fc()` returns at the moment of re-render.
|
|
106
119
|
|
|
107
120
|
## Architecture
|
|
108
121
|
|
|
@@ -114,7 +127,7 @@ flowchart LR
|
|
|
114
127
|
|
|
115
128
|
subgraph OpencodeTUI["opencode TUI process (binary)"]
|
|
116
129
|
direction TB
|
|
117
|
-
Config[("tui.json<br/>plugin: [
|
|
130
|
+
Config[("tui.json<br/>plugin: [opencode-buddy]")]
|
|
118
131
|
Runtime[TuiPluginRuntime]
|
|
119
132
|
Sidebar[Sidebar component]
|
|
120
133
|
Prompt[Prompt component]
|
|
@@ -132,13 +145,15 @@ flowchart LR
|
|
|
132
145
|
direction TB
|
|
133
146
|
TUIEntry["tui entry<br/>(id: opencode-buddy)"]
|
|
134
147
|
View[View component<br/>SolidJS]
|
|
135
|
-
|
|
148
|
+
FrameSignal[("frame signal<br/>(plugin-level)")]
|
|
149
|
+
AnimTick[setInterval 300ms<br/>frame++ gated by energy]
|
|
136
150
|
RefreshTick[setInterval 1500ms<br/>mtime poll]
|
|
137
151
|
SlashCmds["slash commands<br/>feed / play / rest /<br/>status / rename / switch"]
|
|
138
152
|
|
|
153
|
+
TUIEntry --> FrameSignal
|
|
139
154
|
TUIEntry --> View
|
|
140
155
|
TUIEntry --> SlashCmds
|
|
141
|
-
|
|
156
|
+
FrameSignal --> AnimTick
|
|
142
157
|
View --> RefreshTick
|
|
143
158
|
end
|
|
144
159
|
|
|
@@ -164,6 +179,7 @@ flowchart LR
|
|
|
164
179
|
Bus -->|session.idle| View
|
|
165
180
|
Bus -->|session.error| View
|
|
166
181
|
View -->|write derived state| StateFile
|
|
182
|
+
AnimTick -->|reads energy| StateFile
|
|
167
183
|
|
|
168
184
|
classDef store fill:#1e293b,stroke:#64748b,color:#f1f5f9
|
|
169
185
|
class StateFile store
|
|
@@ -172,21 +188,23 @@ flowchart LR
|
|
|
172
188
|
### Boot flow
|
|
173
189
|
|
|
174
190
|
1. opencode reads `~/.config/opencode/tui.json` and discovers the buddy entry under `plugin`.
|
|
175
|
-
2. The TUI runtime loads `tui-plugin.jsx` from `opencode-buddy
|
|
176
|
-
3. The `tui(api)` function runs once
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
6
|
|
180
|
-
|
|
191
|
+
2. The TUI runtime loads `tui-plugin.jsx` from `opencode-buddy` (via `exports["./tui"]` in `package.json`).
|
|
192
|
+
3. The `tui(api)` function runs once. It does three things:
|
|
193
|
+
- Sets up a **plugin-level frame signal** and a `setInterval` (300 ms) that ticks it. The frame signal is passed into the View as a prop, so the View is purely a consumer — its own lifecycle does not own the timer. This means slot re-renders cannot freeze the animation.
|
|
194
|
+
- Registers the `sidebar_content` slot. The slot renderer is cached on first call so the View instance is reused across re-renders.
|
|
195
|
+
- Registers 6 slash commands on the keymap.
|
|
196
|
+
4. The TUI's `<Slot name="sidebar_content" />` (in the sidebar component) resolves to the buddy's cached `View`. The View reads the frame signal and re-renders the buddy art on every tick.
|
|
197
|
+
5. The View also runs a 1500 ms timer that polls `state.json` mtime. When the file changes, the View reloads and re-renders. Slash commands write to `state.json` directly, so the next poll picks up the change.
|
|
198
|
+
6. The `setInterval` for the frame signal also reads `state.json` each tick. If the buddy is `sleeping` or has energy < 20, the frame does not advance — the animation visually pauses while the buddy rests.
|
|
181
199
|
|
|
182
200
|
### Why two config files with the same spec?
|
|
183
201
|
|
|
184
202
|
opencode has separate plugin registries for the **server** (LLM tools, file watching) and the **TUI** (sidebar slots, slash commands, keybindings). When the same package spec appears in both, opencode looks at the package's `exports` field to pick the right entrypoint:
|
|
185
203
|
|
|
186
|
-
- `opencode.json` → `kind: "server"` → loads `src/server-plugin.js` via `main` (
|
|
187
|
-
- `tui.json` → `kind: "tui"` → loads `src/tui-plugin.jsx` via `exports["./tui"]`
|
|
204
|
+
- `opencode.json` → `kind: "server"` → loads `src/server-plugin.js` via `main` (no-op since 0.3.x)
|
|
205
|
+
- `tui.json` → `kind: "tui"` → loads `src/tui-plugin.jsx` via `exports["./tui"]`
|
|
188
206
|
|
|
189
|
-
|
|
207
|
+
Slash commands update state instantly without round-tripping through the LLM, which is the right UX for "I want to feed my pet right now" interactions.
|
|
190
208
|
|
|
191
209
|
## Project layout
|
|
192
210
|
|
|
@@ -195,10 +213,12 @@ opencode-buddy/
|
|
|
195
213
|
├── package.json
|
|
196
214
|
├── README.md
|
|
197
215
|
├── LICENSE
|
|
216
|
+
├── scripts/
|
|
217
|
+
│ └── postinstall.mjs # auto-registers plugin in opencode.json + tui.json
|
|
198
218
|
└── src/
|
|
199
|
-
├── tui-plugin.jsx # TUI plugin: slot + slash commands +
|
|
219
|
+
├── tui-plugin.jsx # TUI plugin: slot + slash commands + frame timer
|
|
200
220
|
├── server-plugin.js # Server plugin: no-op (LLM tool removed in 0.3.x)
|
|
201
|
-
├── species.js # ASCII art + per-species
|
|
221
|
+
├── species.js # ASCII art + per-species palettes + 3-frame idle loop
|
|
202
222
|
├── state.js # state machine: tick, feed, play, rest, rename, switchSpecies, deriveState
|
|
203
223
|
└── persistence.js # atomic read/write of state.json
|
|
204
224
|
```
|
|
@@ -211,7 +231,7 @@ State is held at `~/.config/opencode-buddy/state.json`. `~/.config` resolves via
|
|
|
211
231
|
npm uninstall -g opencode-buddy
|
|
212
232
|
```
|
|
213
233
|
|
|
214
|
-
Then remove `"opencode-buddy"` from `opencode.json` and
|
|
234
|
+
Then remove `"opencode-buddy"` from `opencode.json` and from `tui.json` (the npm uninstall does not auto-edit user config).
|
|
215
235
|
|
|
216
236
|
## License
|
|
217
237
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-buddy",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "A virtual ASCII pet companion that lives in the opencode TUI sidebar. Hatches, feeds, plays, and reacts to what you're coding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server-plugin.js",
|
package/src/species.js
CHANGED
|
@@ -27,27 +27,27 @@ const C = {
|
|
|
27
27
|
const FRAMES = {
|
|
28
28
|
duck: {
|
|
29
29
|
idle: [
|
|
30
|
-
// frame 0: blink open
|
|
30
|
+
// frame 0: bob up, blink open
|
|
31
31
|
[
|
|
32
|
-
` __
|
|
32
|
+
` __ ~ `,
|
|
33
33
|
` <(o )___ `,
|
|
34
34
|
` ( ._> / `,
|
|
35
35
|
` \`--' `,
|
|
36
36
|
` `,
|
|
37
37
|
` ~ idle ~ `,
|
|
38
38
|
],
|
|
39
|
-
// frame 1: blink closed
|
|
39
|
+
// frame 1: bob up, blink closed
|
|
40
40
|
[
|
|
41
|
-
` __
|
|
41
|
+
` __ ~ `,
|
|
42
42
|
` <(- )___ `,
|
|
43
43
|
` ( ._> / `,
|
|
44
44
|
` \`--' `,
|
|
45
45
|
` `,
|
|
46
46
|
` ~ idle ~ `,
|
|
47
47
|
],
|
|
48
|
-
// frame 2:
|
|
48
|
+
// frame 2: same as 0
|
|
49
49
|
[
|
|
50
|
-
` __
|
|
50
|
+
` __ ~ `,
|
|
51
51
|
` <(o )___ `,
|
|
52
52
|
` ( ._> / `,
|
|
53
53
|
` \`--' `,
|
|
@@ -80,19 +80,19 @@ const FRAMES = {
|
|
|
80
80
|
` __ `,
|
|
81
81
|
` <(O )___ `,
|
|
82
82
|
` ( ._> / `,
|
|
83
|
-
` \`--'
|
|
84
|
-
`
|
|
85
|
-
`
|
|
83
|
+
` \`--' `,
|
|
84
|
+
` !! !! `,
|
|
85
|
+
` ! quack `,
|
|
86
86
|
],
|
|
87
87
|
],
|
|
88
88
|
sleeping: [
|
|
89
89
|
[
|
|
90
90
|
` __ `,
|
|
91
|
-
` <(
|
|
92
|
-
` (
|
|
93
|
-
` \`--'
|
|
94
|
-
`
|
|
95
|
-
`
|
|
91
|
+
` <(- )___ `,
|
|
92
|
+
` ( ._> / `,
|
|
93
|
+
` \`--' `,
|
|
94
|
+
` z `,
|
|
95
|
+
` z z z z `,
|
|
96
96
|
],
|
|
97
97
|
],
|
|
98
98
|
},
|
|
@@ -101,7 +101,7 @@ const FRAMES = {
|
|
|
101
101
|
idle: [
|
|
102
102
|
[
|
|
103
103
|
` /\\_/\\ `,
|
|
104
|
-
` ( o.o )
|
|
104
|
+
` ( o.o ) ~ `,
|
|
105
105
|
` > ^ < `,
|
|
106
106
|
` /| |\\ `,
|
|
107
107
|
` (_| |_) `,
|
|
@@ -109,7 +109,7 @@ const FRAMES = {
|
|
|
109
109
|
],
|
|
110
110
|
[
|
|
111
111
|
` /\\_/\\ `,
|
|
112
|
-
` ( -.o )
|
|
112
|
+
` ( -.o ) ~ `,
|
|
113
113
|
` > ^ < `,
|
|
114
114
|
` /| |\\ `,
|
|
115
115
|
` (_| |_) `,
|
|
@@ -117,7 +117,7 @@ const FRAMES = {
|
|
|
117
117
|
],
|
|
118
118
|
[
|
|
119
119
|
` /\\_/\\ `,
|
|
120
|
-
` ( o.o )
|
|
120
|
+
` ( o.o ) ~ `,
|
|
121
121
|
` > ^ < `,
|
|
122
122
|
` /| |\\ `,
|
|
123
123
|
` (_| |_) `,
|
|
@@ -136,101 +136,104 @@ const FRAMES = {
|
|
|
136
136
|
],
|
|
137
137
|
celebrating: [
|
|
138
138
|
[
|
|
139
|
-
` /\\_/\\
|
|
140
|
-
` ( ^.^ )
|
|
141
|
-
` > ~ <
|
|
142
|
-
` /| |\\
|
|
139
|
+
` /\\_/\\ \\o/ `,
|
|
140
|
+
` ( ^.^ ) `,
|
|
141
|
+
` > ~ < \\o/ `,
|
|
142
|
+
` /| |\\ `,
|
|
143
143
|
` (_| |_) `,
|
|
144
|
-
`
|
|
144
|
+
` +5 xp! \\o/ `,
|
|
145
145
|
],
|
|
146
146
|
],
|
|
147
147
|
scared: [
|
|
148
148
|
[
|
|
149
|
-
` /\\_/\\
|
|
150
|
-
` ( O.O )
|
|
149
|
+
` /\\_/\\ !! `,
|
|
150
|
+
` ( O.O ) !! `,
|
|
151
151
|
` > ^ < `,
|
|
152
|
-
` /| |\\
|
|
153
|
-
` (_| |_)
|
|
152
|
+
` /| |\\ !! `,
|
|
153
|
+
` (_| |_) `,
|
|
154
154
|
` !! hiss `,
|
|
155
155
|
],
|
|
156
156
|
],
|
|
157
157
|
sleeping: [
|
|
158
158
|
[
|
|
159
159
|
` /\\_/\\ `,
|
|
160
|
-
` ( -.- )
|
|
160
|
+
` ( -.- ) z `,
|
|
161
161
|
` > ^ < `,
|
|
162
|
-
` /| |\\
|
|
162
|
+
` /| |\\ z `,
|
|
163
163
|
` (_| |_) `,
|
|
164
|
-
` z z z z
|
|
164
|
+
` z z z z `,
|
|
165
165
|
],
|
|
166
166
|
],
|
|
167
167
|
},
|
|
168
168
|
|
|
169
169
|
dragon: {
|
|
170
170
|
idle: [
|
|
171
|
+
// wings trailing right, tail left
|
|
171
172
|
[
|
|
172
|
-
`
|
|
173
|
-
`
|
|
174
|
-
`
|
|
175
|
-
`
|
|
176
|
-
`
|
|
177
|
-
`
|
|
173
|
+
` /^^\\ `,
|
|
174
|
+
` (o o) ~~ `,
|
|
175
|
+
` >w< ~ `,
|
|
176
|
+
` /| |\\ `,
|
|
177
|
+
` (_| |_) ~ `,
|
|
178
|
+
` rawr! `,
|
|
178
179
|
],
|
|
180
|
+
// blink, wings up, tail mid
|
|
179
181
|
[
|
|
180
|
-
`
|
|
181
|
-
`
|
|
182
|
-
`
|
|
183
|
-
`
|
|
184
|
-
`
|
|
185
|
-
`
|
|
182
|
+
` /^^\\ `,
|
|
183
|
+
` (- -) ~~~ `,
|
|
184
|
+
` >w< ~ `,
|
|
185
|
+
` /| |\\ `,
|
|
186
|
+
` (_| |_) ~ `,
|
|
187
|
+
` rawr! `,
|
|
186
188
|
],
|
|
189
|
+
// wings mid, tail right
|
|
187
190
|
[
|
|
188
|
-
`
|
|
189
|
-
`
|
|
190
|
-
`
|
|
191
|
-
`
|
|
192
|
-
`
|
|
193
|
-
`
|
|
191
|
+
` /^^\\ `,
|
|
192
|
+
` (o o) ~~ `,
|
|
193
|
+
` >w< ~ `,
|
|
194
|
+
` /| |\\ `,
|
|
195
|
+
` (_| |_) ~ `,
|
|
196
|
+
` rawr! `,
|
|
194
197
|
],
|
|
195
198
|
],
|
|
196
199
|
working: [
|
|
197
200
|
[
|
|
198
|
-
`
|
|
199
|
-
`
|
|
200
|
-
`
|
|
201
|
-
`
|
|
202
|
-
`
|
|
203
|
-
`
|
|
201
|
+
` /^^\\ ~~~ `,
|
|
202
|
+
` (o o) ~~~ `,
|
|
203
|
+
` >w< ~~ `,
|
|
204
|
+
` /| |\\ ~~~ `,
|
|
205
|
+
` (_| |_) `,
|
|
206
|
+
` forging... `,
|
|
204
207
|
],
|
|
205
208
|
],
|
|
206
209
|
celebrating: [
|
|
207
210
|
[
|
|
208
|
-
`
|
|
209
|
-
`
|
|
210
|
-
`
|
|
211
|
-
`
|
|
212
|
-
`
|
|
213
|
-
`
|
|
211
|
+
` /^^\\ \\o/ `,
|
|
212
|
+
` (^o^) `,
|
|
213
|
+
` >w< \\o/ `,
|
|
214
|
+
` /| |\\ \\o/ `,
|
|
215
|
+
` (_| |_) `,
|
|
216
|
+
` ROAR! \\o/ `,
|
|
214
217
|
],
|
|
215
218
|
],
|
|
216
219
|
scared: [
|
|
217
220
|
[
|
|
218
|
-
`
|
|
219
|
-
`
|
|
220
|
-
`
|
|
221
|
-
`
|
|
222
|
-
`
|
|
223
|
-
`
|
|
221
|
+
` /^^\\ !! `,
|
|
222
|
+
` (O O) !! `,
|
|
223
|
+
` >w< `,
|
|
224
|
+
` /| |\\ !! `,
|
|
225
|
+
` (_| |_) `,
|
|
226
|
+
` ! yikes `,
|
|
224
227
|
],
|
|
225
228
|
],
|
|
226
229
|
sleeping: [
|
|
227
230
|
[
|
|
228
|
-
`
|
|
229
|
-
`
|
|
230
|
-
`
|
|
231
|
-
`
|
|
232
|
-
`
|
|
233
|
-
` z z z z
|
|
231
|
+
` /^^\\ z `,
|
|
232
|
+
` (- -) z `,
|
|
233
|
+
` >w< `,
|
|
234
|
+
` /| |\\ z `,
|
|
235
|
+
` (_| |_) `,
|
|
236
|
+
` z z z z `,
|
|
234
237
|
],
|
|
235
238
|
],
|
|
236
239
|
},
|
|
@@ -239,7 +242,7 @@ const FRAMES = {
|
|
|
239
242
|
idle: [
|
|
240
243
|
[
|
|
241
244
|
` ^___^ `,
|
|
242
|
-
` (o . o)
|
|
245
|
+
` (o . o)~ `,
|
|
243
246
|
` \\|_|_|/ `,
|
|
244
247
|
` \\| |/ `,
|
|
245
248
|
` ) ( `,
|
|
@@ -247,7 +250,7 @@ const FRAMES = {
|
|
|
247
250
|
],
|
|
248
251
|
[
|
|
249
252
|
` ^___^ `,
|
|
250
|
-
` (o - o)
|
|
253
|
+
` (o - o) ~ `,
|
|
251
254
|
` \\|_|_|/ `,
|
|
252
255
|
` \\| |/ `,
|
|
253
256
|
` ) ( `,
|
|
@@ -255,7 +258,7 @@ const FRAMES = {
|
|
|
255
258
|
],
|
|
256
259
|
[
|
|
257
260
|
` ^___^ `,
|
|
258
|
-
` (o . o)
|
|
261
|
+
` (o . o) ~ `,
|
|
259
262
|
` \\|_|_|/ `,
|
|
260
263
|
` \\| |/ `,
|
|
261
264
|
` ) ( `,
|
|
@@ -315,16 +318,16 @@ const FRAMES = {
|
|
|
315
318
|
` beep `,
|
|
316
319
|
],
|
|
317
320
|
[
|
|
318
|
-
` [
|
|
321
|
+
` [- . - ] . `,
|
|
319
322
|
` /|#####|\\ `,
|
|
320
323
|
` / |#####| \\ `,
|
|
321
324
|
` | | `,
|
|
322
325
|
` /| | | |\\ `,
|
|
323
|
-
`
|
|
326
|
+
` bzzt `,
|
|
324
327
|
],
|
|
325
328
|
[
|
|
326
329
|
` [ O . O ] `,
|
|
327
|
-
` /|#####|\\
|
|
330
|
+
` /|#####|\\ . `,
|
|
328
331
|
` / |#####| \\ `,
|
|
329
332
|
` | | `,
|
|
330
333
|
` /| | | |\\ `,
|
|
@@ -384,15 +387,15 @@ const FRAMES = {
|
|
|
384
387
|
` boo `,
|
|
385
388
|
],
|
|
386
389
|
[
|
|
387
|
-
` .-""-.
|
|
388
|
-
` (
|
|
390
|
+
` .-""-. o `,
|
|
391
|
+
` ( - . - ) `,
|
|
389
392
|
` | ~ ~ | `,
|
|
390
393
|
` | | `,
|
|
391
394
|
` \\uuuuu/ `,
|
|
392
395
|
` boo `,
|
|
393
396
|
],
|
|
394
397
|
[
|
|
395
|
-
` .-""-.
|
|
398
|
+
` .-""-. o `,
|
|
396
399
|
` ( o . o ) `,
|
|
397
400
|
` | ~ ~ | `,
|
|
398
401
|
` | | `,
|
|
@@ -500,7 +503,7 @@ const PALETTES = {
|
|
|
500
503
|
export const SPECIES = ["duck", "cat", "dragon", "axolotl", "robot", "ghost"];
|
|
501
504
|
export const STATES = ["idle", "working", "celebrating", "scared", "sleeping"];
|
|
502
505
|
|
|
503
|
-
const FRAME_INTERVAL_MS =
|
|
506
|
+
const FRAME_INTERVAL_MS = 300;
|
|
504
507
|
|
|
505
508
|
export function framesFor(species, state) {
|
|
506
509
|
const list = FRAMES[species]?.[state] ?? FRAMES.duck.idle;
|
|
@@ -529,6 +532,9 @@ export function renderFrame(species, state, frameIdx) {
|
|
|
529
532
|
const frames = framesFor(species, state);
|
|
530
533
|
const i = ((frameIdx % frames.length) + frames.length) % frames.length;
|
|
531
534
|
const palette = PALETTES[species];
|
|
535
|
+
// Idle motion lives in the art itself (wagging tails, flapping wings,
|
|
536
|
+
// bobbing bodies) so the For reconciliation always sees new content
|
|
537
|
+
// each frame and re-renders. No whitespace trickery required.
|
|
532
538
|
return frames[i].map((row) => {
|
|
533
539
|
let out = "";
|
|
534
540
|
for (const ch of row) out += palette(ch);
|
package/src/tui-plugin.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { createSignal, createMemo, onCleanup
|
|
2
|
+
import { createSignal, createMemo, onCleanup } from "solid-js"
|
|
3
3
|
import * as persistence from "./persistence.js"
|
|
4
4
|
import {
|
|
5
5
|
tick as tickState,
|
|
@@ -20,8 +20,8 @@ const REFRESH_TICK_MS = 1500
|
|
|
20
20
|
function View(props) {
|
|
21
21
|
const api = props.api
|
|
22
22
|
const theme = () => api.theme.current
|
|
23
|
+
const frame = props.frame
|
|
23
24
|
const [state, setState] = createSignal(null)
|
|
24
|
-
const [frame, setFrame] = createSignal(0)
|
|
25
25
|
const [lastMtime, setLastMtime] = createSignal(0)
|
|
26
26
|
const [tick, setTick] = createSignal(0)
|
|
27
27
|
|
|
@@ -71,16 +71,17 @@ function View(props) {
|
|
|
71
71
|
const refreshTimer = setInterval(refresh, REFRESH_TICK_MS)
|
|
72
72
|
onCleanup(() => clearInterval(refreshTimer))
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
onCleanup(() => clearInterval(animTimer))
|
|
74
|
+
// The frame signal lives at the plugin level (see tui() below). It
|
|
75
|
+
// is passed in as a prop so this View can read its current value
|
|
76
|
+
// without owning a timer of its own.
|
|
78
77
|
|
|
79
78
|
const current = state
|
|
80
79
|
const species = () => {
|
|
81
80
|
const v = current()
|
|
82
81
|
return v ? v.species : "duck"
|
|
83
82
|
}
|
|
83
|
+
// Only animate when the buddy is awake (not sleeping). Sleeping
|
|
84
|
+
// buddies hold a single frame so the user can see they're resting.
|
|
84
85
|
const buddyState = () => {
|
|
85
86
|
const v = current()
|
|
86
87
|
return v ? v.state : "idle"
|
|
@@ -106,12 +107,18 @@ function View(props) {
|
|
|
106
107
|
return theme().text
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
const SPINNER = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"]
|
|
111
|
+
const spinner = () => SPINNER[frame() % SPINNER.length]
|
|
112
|
+
|
|
109
113
|
return (
|
|
110
114
|
<box>
|
|
111
|
-
<text fg={theme().accent}>{current() ? current().name + " the " + current().species : "BUDDY"}</text>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
</
|
|
115
|
+
<text fg={theme().accent}>{spinner()} {current() ? current().name + " the " + current().species : "BUDDY"}</text>
|
|
116
|
+
<text fg={speciesColor()}>{stripAnsi(lines()[0] ?? "")}</text>
|
|
117
|
+
<text fg={speciesColor()}>{stripAnsi(lines()[1] ?? "")}</text>
|
|
118
|
+
<text fg={speciesColor()}>{stripAnsi(lines()[2] ?? "")}</text>
|
|
119
|
+
<text fg={speciesColor()}>{stripAnsi(lines()[3] ?? "")}</text>
|
|
120
|
+
<text fg={speciesColor()}>{stripAnsi(lines()[4] ?? "")}</text>
|
|
121
|
+
<text fg={speciesColor()}>{stripAnsi(lines()[5] ?? "")}</text>
|
|
115
122
|
<text fg={theme().textMuted}>hunger {barStr(current()?.hunger ?? 0)} {Math.floor(current()?.hunger ?? 0)}</text>
|
|
116
123
|
<text fg={theme().textMuted}>happy {barStr(current()?.happiness ?? 0)} {Math.floor(current()?.happiness ?? 0)}</text>
|
|
117
124
|
<text fg={theme().textMuted}>energy {barStr(current()?.energy ?? 0)} {Math.floor(current()?.energy ?? 0)}</text>
|
|
@@ -121,11 +128,34 @@ function View(props) {
|
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
const tui = async (api) => {
|
|
131
|
+
// The animation timer lives at the plugin level so it survives View
|
|
132
|
+
// remounts. We expose a single global frame counter via a SolidJS
|
|
133
|
+
// signal stored on the api object, which every View can read.
|
|
134
|
+
// This decouples the animation loop from any individual View's
|
|
135
|
+
// lifecycle.
|
|
136
|
+
const [getFrame, setFrame] = createSignal(0)
|
|
137
|
+
api.__buddyFrame = { getFrame, setFrame }
|
|
138
|
+
setInterval(async () => {
|
|
139
|
+
// Read current state from disk. If the buddy is sleeping or has
|
|
140
|
+
// very low energy, freeze the frame so animation visually pauses
|
|
141
|
+
// — a sleeping buddy should not be flickering.
|
|
142
|
+
const s = await persistence.load()
|
|
143
|
+
const sleeping = s && (s.state === "sleeping" || s.energy < 20)
|
|
144
|
+
if (!sleeping) setFrame((f) => f + 1)
|
|
145
|
+
}, ANIMATION_TICK_MS)
|
|
146
|
+
|
|
147
|
+
// Cache the View element so slot re-renders don't recreate it.
|
|
148
|
+
const viewCache = new Map()
|
|
149
|
+
|
|
124
150
|
api.slots.register({
|
|
125
151
|
order: 50,
|
|
126
152
|
slots: {
|
|
127
153
|
sidebar_content(_ctx, props) {
|
|
128
|
-
|
|
154
|
+
const key = props.session_id ?? "default"
|
|
155
|
+
if (!viewCache.has(key)) {
|
|
156
|
+
viewCache.set(key, <View api={api} session_id={props.session_id} frame={getFrame} />)
|
|
157
|
+
}
|
|
158
|
+
return viewCache.get(key)
|
|
129
159
|
},
|
|
130
160
|
},
|
|
131
161
|
})
|