vibe-monitor 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.
- package/README.md +95 -196
- package/assets/demo.gif +0 -0
- package/bin/cli.js +7 -2
- package/index.html +2 -1
- package/main.js +12 -0
- package/package.json +5 -4
- package/preload.js +2 -1
- package/renderer.js +12 -5
- package/shared/animation.js +29 -0
- package/shared/character.js +79 -0
- package/shared/config.js +146 -0
- package/shared/effects.js +240 -0
- package/shared/icons.js +93 -0
- package/shared/styles.css +129 -0
- package/shared/utils.js +77 -0
- package/styles.css +11 -0
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# Vibe Monitor
|
|
1
|
+
# Vibe Monitor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI coding assistant status monitor with pixel art character.
|
|
4
|
+
|
|
5
|
+

|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
@@ -10,259 +12,145 @@ Electron-based desktop app for real-time monitoring of AI coding assistants (Cla
|
|
|
10
12
|
- **HTTP API**: Easy integration with IDE hooks (Claude Code, Kiro)
|
|
11
13
|
- **Draggable**: Move the window to any position
|
|
12
14
|
|
|
13
|
-
##
|
|
15
|
+
## Quick Start
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
17
|
-
npm install
|
|
18
|
+
npx vibe-monitor
|
|
18
19
|
```
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
### Run the App
|
|
21
|
+
### Stop
|
|
23
22
|
|
|
24
23
|
```bash
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Update Status via HTTP API
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
# Change to working state
|
|
32
|
-
curl -X POST http://127.0.0.1:19280/status \
|
|
33
|
-
-H "Content-Type: application/json" \
|
|
34
|
-
-d '{"state":"working","tool":"Bash","project":"my-project","model":"opus","memory":"45%"}'
|
|
24
|
+
curl -X POST http://127.0.0.1:19280/quit
|
|
35
25
|
|
|
36
|
-
#
|
|
37
|
-
|
|
26
|
+
# or
|
|
27
|
+
pkill -f vibe-monitor
|
|
28
|
+
killall Electron
|
|
38
29
|
```
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
#### Claude Code
|
|
43
|
-
|
|
44
|
-
Uses `hooks/vibe-monitor.sh` - see [main README](../README.md#claude-code-setup) for setup.
|
|
45
|
-
|
|
46
|
-
#### Kiro IDE
|
|
31
|
+
## Installation
|
|
47
32
|
|
|
48
|
-
|
|
33
|
+
### From npm
|
|
49
34
|
|
|
50
35
|
```bash
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
cp config/kiro/hooks/*.kiro.hook ~/.kiro/hooks/
|
|
54
|
-
|
|
55
|
-
# Or project-level installation
|
|
56
|
-
# cp config/kiro/hooks/*.kiro.hook your-project/.kiro/hooks/
|
|
36
|
+
npm install -g vibe-monitor
|
|
37
|
+
vibe-monitor
|
|
57
38
|
```
|
|
58
39
|
|
|
59
|
-
|
|
60
|
-
- `vibe-monitor-agent-spawn.kiro.hook` - Sends `start` on `agentSpawn`
|
|
61
|
-
- `vibe-monitor-prompt-submit.kiro.hook` - Sends `working` on `promptSubmit`
|
|
62
|
-
- `vibe-monitor-pre-tool-use.kiro.hook` - Sends `working` on `preToolUse`
|
|
63
|
-
- `vibe-monitor-agent-stop.kiro.hook` - Sends `idle` on `agentStop`
|
|
40
|
+
### From Source
|
|
64
41
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
| **Kiro IDE** | `.kiro.hook` files in `.kiro/hooks/` | ✅ Supported |
|
|
71
|
-
|
|
72
|
-
## States & Characters
|
|
73
|
-
|
|
74
|
-
See [main README](../README.md#state-display) for details on states, animations, and characters.
|
|
75
|
-
|
|
76
|
-
## API
|
|
77
|
-
|
|
78
|
-
### POST /status
|
|
79
|
-
|
|
80
|
-
Update status
|
|
81
|
-
|
|
82
|
-
```json
|
|
83
|
-
{
|
|
84
|
-
"state": "working",
|
|
85
|
-
"event": "PreToolUse",
|
|
86
|
-
"tool": "Bash",
|
|
87
|
-
"project": "vibe-monitor",
|
|
88
|
-
"model": "opus",
|
|
89
|
-
"memory": "45%",
|
|
90
|
-
"character": "clawd"
|
|
91
|
-
}
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/nalbam/vibe-monitor.git
|
|
44
|
+
cd vibe-monitor/desktop
|
|
45
|
+
npm install
|
|
46
|
+
npm start
|
|
92
47
|
```
|
|
93
48
|
|
|
94
|
-
|
|
49
|
+
## States
|
|
95
50
|
|
|
96
|
-
|
|
51
|
+
| State | Color | Description |
|
|
52
|
+
|-------|-------|-------------|
|
|
53
|
+
| `start` | Cyan | Session begins |
|
|
54
|
+
| `idle` | Green | Waiting for input |
|
|
55
|
+
| `thinking` | Purple | Processing prompt |
|
|
56
|
+
| `working` | Blue | Tool executing |
|
|
57
|
+
| `notification` | Yellow | User input needed |
|
|
58
|
+
| `done` | Green | Tool completed |
|
|
59
|
+
| `sleep` | Navy | 10min inactivity |
|
|
97
60
|
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
"state": "working",
|
|
101
|
-
"project": "vibe-monitor",
|
|
102
|
-
"tool": "Bash",
|
|
103
|
-
"model": "opus",
|
|
104
|
-
"memory": "45%"
|
|
105
|
-
}
|
|
106
|
-
```
|
|
61
|
+
## Characters
|
|
107
62
|
|
|
108
|
-
|
|
63
|
+
- **clawd** (default): Orange pixel art character
|
|
64
|
+
- **kiro**: White ghost character
|
|
109
65
|
|
|
110
|
-
|
|
66
|
+
## IDE Integration
|
|
111
67
|
|
|
112
|
-
|
|
113
|
-
{
|
|
114
|
-
"status": "ok"
|
|
115
|
-
}
|
|
116
|
-
```
|
|
68
|
+
### Claude Code
|
|
117
69
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Show window and position to top-right corner
|
|
70
|
+
Add to `~/.claude/settings.json`:
|
|
121
71
|
|
|
122
72
|
```json
|
|
123
73
|
{
|
|
124
|
-
"
|
|
74
|
+
"hooks": {
|
|
75
|
+
"PreToolUse": [
|
|
76
|
+
{
|
|
77
|
+
"matcher": "",
|
|
78
|
+
"hooks": [
|
|
79
|
+
{
|
|
80
|
+
"type": "command",
|
|
81
|
+
"command": "curl -s -X POST http://127.0.0.1:19280/status -H 'Content-Type: application/json' -d \"$(jq -nc --arg state working --arg event PreToolUse --arg tool \"$CLAUDE_TOOL_NAME\" --arg project \"$(basename $PWD)\" '{state: $state, event: $event, tool: $tool, project: $project}')\" > /dev/null 2>&1 || true"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
125
87
|
}
|
|
126
88
|
```
|
|
127
89
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Get display and window debug information (useful for troubleshooting positioning issues)
|
|
131
|
-
|
|
132
|
-
```json
|
|
133
|
-
{
|
|
134
|
-
"primaryDisplay": {
|
|
135
|
-
"bounds": { "x": 0, "y": 0, "width": 1920, "height": 1080 },
|
|
136
|
-
"workArea": { "x": 0, "y": 0, "width": 1920, "height": 1040 },
|
|
137
|
-
"scaleFactor": 1
|
|
138
|
-
},
|
|
139
|
-
"window": { "x": 1748, "y": 0, "width": 172, "height": 348 },
|
|
140
|
-
"platform": "darwin"
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## WSL (Windows Subsystem for Linux)
|
|
145
|
-
|
|
146
|
-
Running the Electron app on WSL requires WSLg (Windows 11) and additional dependencies.
|
|
147
|
-
|
|
148
|
-
### Prerequisites
|
|
149
|
-
|
|
150
|
-
1. **Windows 11** with WSLg support
|
|
151
|
-
2. **Update WSL**:
|
|
152
|
-
```bash
|
|
153
|
-
wsl --update
|
|
154
|
-
```
|
|
90
|
+
See [GitHub repository](https://github.com/nalbam/vibe-monitor) for full hook configuration.
|
|
155
91
|
|
|
156
|
-
###
|
|
92
|
+
### Kiro IDE
|
|
157
93
|
|
|
158
|
-
|
|
94
|
+
Copy hook files from the repository to `~/.kiro/hooks/`:
|
|
159
95
|
|
|
160
96
|
```bash
|
|
161
|
-
|
|
162
|
-
sudo apt-get update && sudo apt-get install -y \
|
|
163
|
-
libasound2t64 \
|
|
164
|
-
libatk1.0-0 \
|
|
165
|
-
libatk-bridge2.0-0 \
|
|
166
|
-
libcups2 \
|
|
167
|
-
libdrm2 \
|
|
168
|
-
libgbm1 \
|
|
169
|
-
libgtk-3-0 \
|
|
170
|
-
libnss3 \
|
|
171
|
-
libxcomposite1 \
|
|
172
|
-
libxdamage1 \
|
|
173
|
-
libxfixes3 \
|
|
174
|
-
libxkbcommon0 \
|
|
175
|
-
libxrandr2 \
|
|
176
|
-
libxshmfence1 \
|
|
177
|
-
libglu1-mesa
|
|
178
|
-
|
|
179
|
-
# Ubuntu 22.04 (Jammy) or earlier
|
|
180
|
-
sudo apt-get update && sudo apt-get install -y \
|
|
181
|
-
libasound2 \
|
|
182
|
-
libatk1.0-0 \
|
|
183
|
-
libatk-bridge2.0-0 \
|
|
184
|
-
libcups2 \
|
|
185
|
-
libdrm2 \
|
|
186
|
-
libgbm1 \
|
|
187
|
-
libgtk-3-0 \
|
|
188
|
-
libnss3 \
|
|
189
|
-
libxcomposite1 \
|
|
190
|
-
libxdamage1 \
|
|
191
|
-
libxfixes3 \
|
|
192
|
-
libxkbcommon0 \
|
|
193
|
-
libxrandr2
|
|
97
|
+
curl -sL https://raw.githubusercontent.com/nalbam/vibe-monitor/main/config/kiro/hooks/vibe-monitor-pre-tool-use.kiro.hook -o ~/.kiro/hooks/vibe-monitor-pre-tool-use.kiro.hook
|
|
194
98
|
```
|
|
195
99
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
cd desktop
|
|
200
|
-
npm install
|
|
201
|
-
npm start
|
|
202
|
-
```
|
|
100
|
+
## API
|
|
203
101
|
|
|
204
|
-
###
|
|
102
|
+
### POST /status
|
|
205
103
|
|
|
206
|
-
|
|
104
|
+
Update status:
|
|
207
105
|
|
|
208
|
-
Install the audio library:
|
|
209
106
|
```bash
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# Ubuntu 22.04 or earlier
|
|
214
|
-
sudo apt-get install -y libasound2
|
|
107
|
+
curl -X POST http://127.0.0.1:19280/status \
|
|
108
|
+
-H "Content-Type: application/json" \
|
|
109
|
+
-d '{"state":"working","tool":"Bash","project":"my-project"}'
|
|
215
110
|
```
|
|
216
111
|
|
|
217
|
-
**
|
|
112
|
+
**Fields:**
|
|
218
113
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
114
|
+
| Field | Description |
|
|
115
|
+
|-------|-------------|
|
|
116
|
+
| `state` | `start`, `idle`, `thinking`, `working`, `notification`, `done`, `sleep` |
|
|
117
|
+
| `event` | `PreToolUse`, `PostToolUse`, etc. |
|
|
118
|
+
| `tool` | Tool name (e.g., `Bash`, `Read`, `Edit`) |
|
|
119
|
+
| `project` | Project name |
|
|
120
|
+
| `model` | Model name (e.g., `opus`, `sonnet`) |
|
|
121
|
+
| `memory` | Memory usage (e.g., `45%`) |
|
|
122
|
+
| `character` | `clawd` or `kiro` |
|
|
123
|
+
|
|
124
|
+
### GET /status
|
|
224
125
|
|
|
225
|
-
|
|
126
|
+
Get current status:
|
|
226
127
|
|
|
227
|
-
Ensure WSLg is working:
|
|
228
128
|
```bash
|
|
229
|
-
|
|
230
|
-
sudo apt-get install -y x11-apps
|
|
231
|
-
xclock
|
|
129
|
+
curl http://127.0.0.1:19280/status
|
|
232
130
|
```
|
|
233
131
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
## Build
|
|
132
|
+
### GET /health
|
|
237
133
|
|
|
238
|
-
|
|
134
|
+
Health check:
|
|
239
135
|
|
|
240
136
|
```bash
|
|
241
|
-
|
|
137
|
+
curl http://127.0.0.1:19280/health
|
|
242
138
|
```
|
|
243
139
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
```bash
|
|
247
|
-
npm run build:dmg
|
|
248
|
-
```
|
|
140
|
+
### POST /show
|
|
249
141
|
|
|
250
|
-
|
|
142
|
+
Show window:
|
|
251
143
|
|
|
252
144
|
```bash
|
|
253
|
-
|
|
145
|
+
curl -X POST http://127.0.0.1:19280/show
|
|
254
146
|
```
|
|
255
147
|
|
|
256
|
-
|
|
148
|
+
### POST /quit
|
|
257
149
|
|
|
258
|
-
|
|
259
|
-
npm run build:linux
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Build for all platforms:
|
|
150
|
+
Quit application:
|
|
263
151
|
|
|
264
152
|
```bash
|
|
265
|
-
|
|
153
|
+
curl -X POST http://127.0.0.1:19280/quit
|
|
266
154
|
```
|
|
267
155
|
|
|
268
156
|
## Tray Menu
|
|
@@ -278,4 +166,15 @@ Click the system tray icon to:
|
|
|
278
166
|
|
|
279
167
|
Default HTTP server port: `19280`
|
|
280
168
|
|
|
281
|
-
|
|
169
|
+
## Build
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm run build:mac # macOS
|
|
173
|
+
npm run build:win # Windows
|
|
174
|
+
npm run build:linux # Linux
|
|
175
|
+
npm run build:all # All platforms
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
package/assets/demo.gif
ADDED
|
Binary file
|
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,11 @@ const path = require('path');
|
|
|
6
6
|
const electron = require('electron');
|
|
7
7
|
const appPath = path.join(__dirname, '..');
|
|
8
8
|
|
|
9
|
-
spawn(electron, [appPath], {
|
|
10
|
-
|
|
9
|
+
const child = spawn(electron, [appPath], {
|
|
10
|
+
detached: true,
|
|
11
|
+
stdio: 'ignore'
|
|
11
12
|
});
|
|
13
|
+
|
|
14
|
+
child.unref();
|
|
15
|
+
|
|
16
|
+
console.log('Vibe Monitor started (http://127.0.0.1:19280)');
|
package/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Vibe Monitor</title>
|
|
7
|
-
<link rel="stylesheet" href="
|
|
7
|
+
<link rel="stylesheet" href="./shared/styles.css">
|
|
8
8
|
<link rel="stylesheet" href="styles.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
<div class="memory-bar-container" id="memory-bar-container">
|
|
49
49
|
<div class="memory-bar" id="memory-bar"></div>
|
|
50
50
|
</div>
|
|
51
|
+
<div class="version-text" id="version-text"></div>
|
|
51
52
|
</div>
|
|
52
53
|
</div>
|
|
53
54
|
|
package/main.js
CHANGED
|
@@ -256,6 +256,10 @@ function updateTrayMenu() {
|
|
|
256
256
|
label: `HTTP Server: localhost:${HTTP_PORT}`,
|
|
257
257
|
enabled: false
|
|
258
258
|
},
|
|
259
|
+
{
|
|
260
|
+
label: `Version: ${app.getVersion()}`,
|
|
261
|
+
enabled: false
|
|
262
|
+
},
|
|
259
263
|
{ type: 'separator' },
|
|
260
264
|
{
|
|
261
265
|
label: 'Quit',
|
|
@@ -445,6 +449,10 @@ function startHttpServer() {
|
|
|
445
449
|
window: windowBounds,
|
|
446
450
|
platform: process.platform
|
|
447
451
|
}, null, 2));
|
|
452
|
+
} else if (req.method === 'POST' && req.url === '/quit') {
|
|
453
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
454
|
+
res.end(JSON.stringify({ success: true }));
|
|
455
|
+
setTimeout(() => app.quit(), 100);
|
|
448
456
|
} else {
|
|
449
457
|
res.writeHead(404);
|
|
450
458
|
res.end('Not Found');
|
|
@@ -464,6 +472,10 @@ function startHttpServer() {
|
|
|
464
472
|
}
|
|
465
473
|
|
|
466
474
|
// IPC handlers
|
|
475
|
+
ipcMain.handle('get-version', () => {
|
|
476
|
+
return app.getVersion();
|
|
477
|
+
});
|
|
478
|
+
|
|
467
479
|
ipcMain.on('close-window', () => {
|
|
468
480
|
if (mainWindow) {
|
|
469
481
|
mainWindow.hide();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-monitor",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "AI coding assistant status monitor",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"vibe-monitor": "./bin/cli.js"
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"author": "nalbam",
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"canvas": "^3.2.1",
|
|
21
22
|
"electron": "^33.0.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"canvas": "^3.2.1",
|
|
25
25
|
"electron-builder": "^25.0.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"index.html",
|
|
32
32
|
"renderer.js",
|
|
33
33
|
"styles.css",
|
|
34
|
-
"assets/"
|
|
34
|
+
"assets/",
|
|
35
|
+
"shared/"
|
|
35
36
|
],
|
|
36
37
|
"build": {
|
|
37
38
|
"appId": "com.nalbam.vibe-monitor",
|
package/preload.js
CHANGED
|
@@ -5,5 +5,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
5
5
|
minimizeWindow: () => ipcRenderer.send('minimize-window'),
|
|
6
6
|
onStateUpdate: (callback) => {
|
|
7
7
|
ipcRenderer.on('state-update', (event, data) => callback(data));
|
|
8
|
-
}
|
|
8
|
+
},
|
|
9
|
+
getVersion: () => ipcRenderer.invoke('get-version')
|
|
9
10
|
});
|
package/renderer.js
CHANGED
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
BLINK_START_FRAME, BLINK_END_FRAME,
|
|
6
6
|
PROJECT_NAME_MAX_LENGTH, PROJECT_NAME_TRUNCATE_AT,
|
|
7
7
|
MODEL_NAME_MAX_LENGTH, MODEL_NAME_TRUNCATE_AT
|
|
8
|
-
} from '
|
|
9
|
-
import { getThinkingText, getWorkingText, updateMemoryBar } from '
|
|
10
|
-
import { initRenderer, drawCharacter } from '
|
|
11
|
-
import { drawInfoIcons } from '
|
|
12
|
-
import { getFloatOffsetX, getFloatOffsetY, needsAnimationRedraw } from '
|
|
8
|
+
} from './shared/config.js';
|
|
9
|
+
import { getThinkingText, getWorkingText, updateMemoryBar } from './shared/utils.js';
|
|
10
|
+
import { initRenderer, drawCharacter } from './shared/character.js';
|
|
11
|
+
import { drawInfoIcons } from './shared/icons.js';
|
|
12
|
+
import { getFloatOffsetX, getFloatOffsetY, needsAnimationRedraw } from './shared/animation.js';
|
|
13
13
|
|
|
14
14
|
// Platform detection: macOS uses emoji, Windows/Linux uses pixel art
|
|
15
15
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
@@ -70,6 +70,13 @@ function init() {
|
|
|
70
70
|
initRenderer(ctx);
|
|
71
71
|
initDomCache();
|
|
72
72
|
|
|
73
|
+
// Display version (set in HTML, updated dynamically if needed)
|
|
74
|
+
if (window.electronAPI?.getVersion) {
|
|
75
|
+
window.electronAPI.getVersion().then(version => {
|
|
76
|
+
document.getElementById('version-text').textContent = `v${version}`;
|
|
77
|
+
}).catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
updateDisplay();
|
|
74
81
|
startAnimation();
|
|
75
82
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Shared animation utilities
|
|
2
|
+
import { FLOAT_AMPLITUDE_X, FLOAT_AMPLITUDE_Y } from './config.js';
|
|
3
|
+
|
|
4
|
+
// Calculate floating X offset using cosine wave (~3.2 second cycle at 100ms interval)
|
|
5
|
+
export function getFloatOffsetX(animFrame) {
|
|
6
|
+
const angle = (animFrame % 32) * (2.0 * Math.PI / 32.0);
|
|
7
|
+
return Math.cos(angle) * FLOAT_AMPLITUDE_X;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Calculate floating Y offset using sine wave
|
|
11
|
+
export function getFloatOffsetY(animFrame) {
|
|
12
|
+
const angle = (animFrame % 32) * (2.0 * Math.PI / 32.0);
|
|
13
|
+
return Math.sin(angle) * FLOAT_AMPLITUDE_Y;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check if animation frame requires redraw for given state
|
|
17
|
+
export function needsAnimationRedraw(state, animFrame, blinkFrame) {
|
|
18
|
+
switch (state) {
|
|
19
|
+
case 'start':
|
|
20
|
+
case 'thinking':
|
|
21
|
+
case 'working':
|
|
22
|
+
case 'sleep':
|
|
23
|
+
return true; // Always animate these states
|
|
24
|
+
case 'idle':
|
|
25
|
+
return blinkFrame === 30 || blinkFrame === 31; // Only during blink
|
|
26
|
+
default:
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CHAR_SIZE, SCALE, CHARACTER_CONFIG, DEFAULT_CHARACTER, states } from './config.js';
|
|
2
|
+
import { drawEyes, drawMatrixBackground } from './effects.js';
|
|
3
|
+
|
|
4
|
+
let ctx = null;
|
|
5
|
+
|
|
6
|
+
// Character images cache
|
|
7
|
+
const characterImages = {};
|
|
8
|
+
let imagesLoaded = false;
|
|
9
|
+
|
|
10
|
+
// Image paths for each character
|
|
11
|
+
const CHARACTER_IMAGES = {
|
|
12
|
+
clawd: '../images/clawd-128.png',
|
|
13
|
+
kiro: '../images/kiro-128.png'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Preload character images
|
|
17
|
+
function preloadImages() {
|
|
18
|
+
if (imagesLoaded) return Promise.resolve();
|
|
19
|
+
|
|
20
|
+
const promises = Object.entries(CHARACTER_IMAGES).map(([name, path]) => {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const img = new Image();
|
|
23
|
+
img.onload = () => {
|
|
24
|
+
characterImages[name] = img;
|
|
25
|
+
resolve();
|
|
26
|
+
};
|
|
27
|
+
img.onerror = () => {
|
|
28
|
+
console.warn(`Failed to load image for ${name}: ${path}`);
|
|
29
|
+
resolve();
|
|
30
|
+
};
|
|
31
|
+
img.src = path;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return Promise.all(promises).then(() => {
|
|
36
|
+
imagesLoaded = true;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if character has image
|
|
41
|
+
function hasImage(characterName) {
|
|
42
|
+
return characterImages[characterName] !== undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Initialize renderer with canvas context
|
|
46
|
+
export function initRenderer(canvasCtx) {
|
|
47
|
+
ctx = canvasCtx;
|
|
48
|
+
preloadImages();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Helper: draw scaled rect
|
|
52
|
+
export function drawRect(x, y, w, h, color) {
|
|
53
|
+
ctx.fillStyle = color;
|
|
54
|
+
ctx.fillRect(x * SCALE, y * SCALE, w * SCALE, h * SCALE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Draw character (128x128, scaled 2x from 64x64)
|
|
58
|
+
export function drawCharacter(eyeType, currentState, currentCharacter, animFrame) {
|
|
59
|
+
const state = states[currentState] || states.idle;
|
|
60
|
+
const char = CHARACTER_CONFIG[currentCharacter] || CHARACTER_CONFIG[DEFAULT_CHARACTER];
|
|
61
|
+
|
|
62
|
+
// Clear with background color
|
|
63
|
+
ctx.fillStyle = state.bgColor;
|
|
64
|
+
ctx.fillRect(0, 0, CHAR_SIZE, CHAR_SIZE);
|
|
65
|
+
|
|
66
|
+
// Draw matrix background for working state (behind character)
|
|
67
|
+
if (currentState === 'working') {
|
|
68
|
+
drawMatrixBackground(animFrame, drawRect, CHAR_SIZE / SCALE, char.body);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Draw character image
|
|
72
|
+
if (hasImage(currentCharacter)) {
|
|
73
|
+
const img = characterImages[currentCharacter];
|
|
74
|
+
ctx.drawImage(img, 0, 0, CHAR_SIZE, CHAR_SIZE);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Draw eyes (for all characters)
|
|
78
|
+
drawEyes(eyeType, char, animFrame, drawRect);
|
|
79
|
+
}
|
package/shared/config.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Character size (128x128, doubled from 64)
|
|
2
|
+
export const CHAR_SIZE = 128;
|
|
3
|
+
export const SCALE = 2;
|
|
4
|
+
|
|
5
|
+
// Colors
|
|
6
|
+
export const COLOR_EYE = '#000000';
|
|
7
|
+
export const COLOR_WHITE = '#FFFFFF';
|
|
8
|
+
|
|
9
|
+
// State configuration
|
|
10
|
+
export const states = {
|
|
11
|
+
start: {
|
|
12
|
+
bgColor: '#00CCCC',
|
|
13
|
+
text: 'Hello!',
|
|
14
|
+
eyeType: 'sparkle',
|
|
15
|
+
showLoading: false,
|
|
16
|
+
textColor: '#000000'
|
|
17
|
+
},
|
|
18
|
+
idle: {
|
|
19
|
+
bgColor: '#00AA00',
|
|
20
|
+
text: 'Ready',
|
|
21
|
+
eyeType: 'normal',
|
|
22
|
+
showLoading: false,
|
|
23
|
+
textColor: '#FFFFFF'
|
|
24
|
+
},
|
|
25
|
+
thinking: {
|
|
26
|
+
bgColor: '#6633CC',
|
|
27
|
+
text: 'Thinking',
|
|
28
|
+
eyeType: 'thinking',
|
|
29
|
+
showLoading: true,
|
|
30
|
+
textColor: '#FFFFFF'
|
|
31
|
+
},
|
|
32
|
+
working: {
|
|
33
|
+
bgColor: '#0066CC',
|
|
34
|
+
text: 'Working',
|
|
35
|
+
eyeType: 'focused',
|
|
36
|
+
showLoading: true,
|
|
37
|
+
textColor: '#FFFFFF'
|
|
38
|
+
},
|
|
39
|
+
notification: {
|
|
40
|
+
bgColor: '#FFCC00',
|
|
41
|
+
text: 'Input?',
|
|
42
|
+
eyeType: 'alert',
|
|
43
|
+
showLoading: false,
|
|
44
|
+
textColor: '#000000'
|
|
45
|
+
},
|
|
46
|
+
done: {
|
|
47
|
+
bgColor: '#00AA00',
|
|
48
|
+
text: 'Done!',
|
|
49
|
+
eyeType: 'happy',
|
|
50
|
+
showLoading: false,
|
|
51
|
+
textColor: '#FFFFFF'
|
|
52
|
+
},
|
|
53
|
+
sleep: {
|
|
54
|
+
bgColor: '#1a1a4e',
|
|
55
|
+
text: 'Zzz...',
|
|
56
|
+
eyeType: 'sleep',
|
|
57
|
+
showLoading: false,
|
|
58
|
+
textColor: '#FFFFFF'
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Character configurations - Single source of truth
|
|
63
|
+
export const CHARACTER_CONFIG = {
|
|
64
|
+
clawd: {
|
|
65
|
+
name: 'clawd',
|
|
66
|
+
displayName: 'Clawd',
|
|
67
|
+
color: '#D97757',
|
|
68
|
+
body: { x: 6, y: 8, w: 52, h: 36 },
|
|
69
|
+
arms: { left: { x: 0, y: 22, w: 6, h: 10 }, right: { x: 58, y: 22, w: 6, h: 10 } },
|
|
70
|
+
legs: [
|
|
71
|
+
{ x: 10, y: 44, w: 6, h: 12 },
|
|
72
|
+
{ x: 18, y: 44, w: 6, h: 12 },
|
|
73
|
+
{ x: 40, y: 44, w: 6, h: 12 },
|
|
74
|
+
{ x: 48, y: 44, w: 6, h: 12 }
|
|
75
|
+
],
|
|
76
|
+
tail: null,
|
|
77
|
+
eyes: { left: { x: 14, y: 22 }, right: { x: 44, y: 22 }, size: 6 },
|
|
78
|
+
isGhost: false
|
|
79
|
+
},
|
|
80
|
+
kiro: {
|
|
81
|
+
name: 'kiro',
|
|
82
|
+
displayName: 'Kiro',
|
|
83
|
+
color: '#FFFFFF',
|
|
84
|
+
// Sprite-based rendering (see sprites.js) - 64x64 sprite
|
|
85
|
+
body: { x: 10, y: 3, w: 44, h: 30 },
|
|
86
|
+
arms: null,
|
|
87
|
+
legs: [],
|
|
88
|
+
tail: [],
|
|
89
|
+
// Eyes positioned in sprite (drawn by drawEyes, not in sprite)
|
|
90
|
+
eyes: { left: { x: 29, y: 21 }, right: { x: 39, y: 21 }, w: 5, h: 8 },
|
|
91
|
+
isGhost: true
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const CHARACTER_NAMES = Object.keys(CHARACTER_CONFIG);
|
|
96
|
+
export const DEFAULT_CHARACTER = 'clawd';
|
|
97
|
+
|
|
98
|
+
// Thinking state texts (random selection)
|
|
99
|
+
export const THINKING_TEXTS = ['Thinking', 'Hmm', 'Let me see'];
|
|
100
|
+
|
|
101
|
+
// Tool-based status texts for working state (lowercase keys)
|
|
102
|
+
export const TOOL_TEXTS = {
|
|
103
|
+
'bash': ['Running', 'Executing', 'Processing'],
|
|
104
|
+
'read': ['Reading', 'Scanning', 'Checking'],
|
|
105
|
+
'edit': ['Editing', 'Modifying', 'Fixing'],
|
|
106
|
+
'write': ['Writing', 'Creating', 'Saving'],
|
|
107
|
+
'grep': ['Searching', 'Finding', 'Looking'],
|
|
108
|
+
'glob': ['Scanning', 'Browsing', 'Finding'],
|
|
109
|
+
'task': ['Thinking', 'Working', 'Planning'],
|
|
110
|
+
'webfetch': ['Fetching', 'Loading', 'Getting'],
|
|
111
|
+
'websearch': ['Searching', 'Googling', 'Looking'],
|
|
112
|
+
'default': ['Working', 'Busy', 'Coding']
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Floating animation constants
|
|
116
|
+
export const FLOAT_AMPLITUDE_X = 3;
|
|
117
|
+
export const FLOAT_AMPLITUDE_Y = 5;
|
|
118
|
+
export const CHAR_X_BASE = 22;
|
|
119
|
+
export const CHAR_Y_BASE = 20;
|
|
120
|
+
|
|
121
|
+
// State timeouts
|
|
122
|
+
export const DONE_TO_IDLE_TIMEOUT = 60 * 1000; // 1 minute
|
|
123
|
+
export const SLEEP_TIMEOUT = 600 * 1000; // 10 minutes
|
|
124
|
+
|
|
125
|
+
// Animation constants
|
|
126
|
+
export const FRAME_INTERVAL = 100; // 100ms per frame
|
|
127
|
+
export const FLOAT_CYCLE_FRAMES = 32; // ~3.2 seconds at 100ms tick
|
|
128
|
+
export const LOADING_DOT_COUNT = 4;
|
|
129
|
+
export const THINKING_ANIMATION_SLOWDOWN = 3; // 3x slower for thinking state
|
|
130
|
+
export const BLINK_START_FRAME = 30;
|
|
131
|
+
export const BLINK_END_FRAME = 31;
|
|
132
|
+
|
|
133
|
+
// Text truncation limits
|
|
134
|
+
export const PROJECT_NAME_MAX_LENGTH = 16;
|
|
135
|
+
export const PROJECT_NAME_TRUNCATE_AT = 13;
|
|
136
|
+
export const MODEL_NAME_MAX_LENGTH = 14;
|
|
137
|
+
export const MODEL_NAME_TRUNCATE_AT = 11;
|
|
138
|
+
|
|
139
|
+
// Matrix effect constants
|
|
140
|
+
export const MATRIX_STREAM_DENSITY = 0.7; // 70% of streams visible
|
|
141
|
+
export const MATRIX_SPEED_MIN = 1;
|
|
142
|
+
export const MATRIX_SPEED_MAX = 6;
|
|
143
|
+
export const MATRIX_COLUMN_WIDTH = 4;
|
|
144
|
+
export const MATRIX_FLICKER_PERIOD = 3;
|
|
145
|
+
export const MATRIX_TAIL_LENGTH_FAST = 8; // speed > 3
|
|
146
|
+
export const MATRIX_TAIL_LENGTH_SLOW = 6;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COLOR_EYE, COLOR_WHITE, CHARACTER_CONFIG, DEFAULT_CHARACTER,
|
|
3
|
+
MATRIX_STREAM_DENSITY, MATRIX_SPEED_MIN, MATRIX_SPEED_MAX,
|
|
4
|
+
MATRIX_COLUMN_WIDTH, MATRIX_FLICKER_PERIOD,
|
|
5
|
+
MATRIX_TAIL_LENGTH_FAST, MATRIX_TAIL_LENGTH_SLOW
|
|
6
|
+
} from './config.js';
|
|
7
|
+
|
|
8
|
+
// Effect color for white characters (orange)
|
|
9
|
+
const COLOR_EFFECT_ALT = '#FFA500';
|
|
10
|
+
|
|
11
|
+
// Sunglasses colors
|
|
12
|
+
const COLOR_SUNGLASSES_FRAME = '#111111';
|
|
13
|
+
const COLOR_SUNGLASSES_LENS = '#001100';
|
|
14
|
+
const COLOR_SUNGLASSES_SHINE = '#003300';
|
|
15
|
+
|
|
16
|
+
// Get eye cover position (used by sunglasses and sleep eyes)
|
|
17
|
+
function getEyeCoverPosition(leftX, rightX, eyeY, eyeW, eyeH, isKiro = false) {
|
|
18
|
+
const lensW = eyeW + 4;
|
|
19
|
+
const lensH = eyeH + 2;
|
|
20
|
+
// Kiro: shift up 2px
|
|
21
|
+
const lensY = eyeY - 1 - (isKiro ? 2 : 0);
|
|
22
|
+
// Kiro: left lens 2px right, right lens 5px right
|
|
23
|
+
const leftLensX = leftX - 2 + (isKiro ? 2 : 0);
|
|
24
|
+
const rightLensX = rightX - 2 + (isKiro ? 5 : 0);
|
|
25
|
+
return { lensW, lensH, lensY, leftLensX, rightLensX };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Draw sunglasses (Matrix style)
|
|
29
|
+
function drawSunglasses(leftX, rightX, eyeY, eyeW, eyeH, drawRect, isKiro = false) {
|
|
30
|
+
const { lensW, lensH, lensY, leftLensX, rightLensX } = getEyeCoverPosition(leftX, rightX, eyeY, eyeW, eyeH, isKiro);
|
|
31
|
+
|
|
32
|
+
// Sunglasses colors (same for all characters)
|
|
33
|
+
const frameColor = COLOR_SUNGLASSES_FRAME;
|
|
34
|
+
const lensColor = COLOR_SUNGLASSES_LENS;
|
|
35
|
+
const shineColor = COLOR_SUNGLASSES_SHINE;
|
|
36
|
+
|
|
37
|
+
// Left lens (dark green tint)
|
|
38
|
+
drawRect(leftLensX, lensY, lensW, lensH, lensColor);
|
|
39
|
+
// Left lens shine
|
|
40
|
+
drawRect(leftLensX + 1, lensY + 1, 2, 1, shineColor);
|
|
41
|
+
|
|
42
|
+
// Right lens (dark green tint)
|
|
43
|
+
drawRect(rightLensX, lensY, lensW, lensH, lensColor);
|
|
44
|
+
// Right lens shine
|
|
45
|
+
drawRect(rightLensX + 1, lensY + 1, 2, 1, shineColor);
|
|
46
|
+
|
|
47
|
+
// Frame - top
|
|
48
|
+
drawRect(leftLensX - 1, lensY - 1, lensW + 2, 1, frameColor);
|
|
49
|
+
drawRect(rightLensX - 1, lensY - 1, lensW + 2, 1, frameColor);
|
|
50
|
+
|
|
51
|
+
// Frame - bottom
|
|
52
|
+
drawRect(leftLensX - 1, lensY + lensH, lensW + 2, 1, frameColor);
|
|
53
|
+
drawRect(rightLensX - 1, lensY + lensH, lensW + 2, 1, frameColor);
|
|
54
|
+
|
|
55
|
+
// Frame - sides
|
|
56
|
+
drawRect(leftLensX - 1, lensY, 1, lensH, frameColor);
|
|
57
|
+
drawRect(leftLensX + lensW, lensY, 1, lensH, frameColor);
|
|
58
|
+
drawRect(rightLensX - 1, lensY, 1, lensH, frameColor);
|
|
59
|
+
drawRect(rightLensX + lensW, lensY, 1, lensH, frameColor);
|
|
60
|
+
|
|
61
|
+
// Bridge (connects two lenses)
|
|
62
|
+
const bridgeY = lensY + Math.floor(lensH / 2);
|
|
63
|
+
drawRect(leftLensX + lensW, bridgeY, rightLensX - leftLensX - lensW, 1, frameColor);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Draw sleep eyes (closed eyes with body color background)
|
|
67
|
+
function drawSleepEyes(leftX, rightX, eyeY, eyeW, eyeH, drawRect, bodyColor, isKiro = false) {
|
|
68
|
+
const { lensW, lensH, lensY, leftLensX, rightLensX } = getEyeCoverPosition(leftX, rightX, eyeY, eyeW, eyeH, isKiro);
|
|
69
|
+
|
|
70
|
+
// Cover original eyes with body color (same area as sunglasses)
|
|
71
|
+
drawRect(leftLensX, lensY, lensW, lensH, bodyColor);
|
|
72
|
+
drawRect(rightLensX, lensY, lensW, lensH, bodyColor);
|
|
73
|
+
|
|
74
|
+
// Draw closed eyes (horizontal lines in the middle)
|
|
75
|
+
const closedEyeY = lensY + Math.floor(lensH / 2);
|
|
76
|
+
const closedEyeH = 2; // 2px thick line
|
|
77
|
+
drawRect(leftLensX + 1, closedEyeY, lensW - 2, closedEyeH, COLOR_EYE);
|
|
78
|
+
drawRect(rightLensX + 1, closedEyeY, lensW - 2, closedEyeH, COLOR_EYE);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Get effect color based on character color
|
|
82
|
+
function getEffectColor(char) {
|
|
83
|
+
return char.color === '#FFFFFF' ? COLOR_EFFECT_ALT : COLOR_WHITE;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Draw eyes (scaled 2x)
|
|
87
|
+
// Note: Eyes are now part of character images, only draw effects and sunglasses
|
|
88
|
+
export function drawEyes(eyeType, char, animFrame, drawRect) {
|
|
89
|
+
char = char || CHARACTER_CONFIG[DEFAULT_CHARACTER];
|
|
90
|
+
const leftX = char.eyes.left.x;
|
|
91
|
+
const rightX = char.eyes.right.x;
|
|
92
|
+
const eyeY = char.eyes.left.y;
|
|
93
|
+
// Support separate width/height or fallback to size
|
|
94
|
+
const eyeW = char.eyes.w || char.eyes.size || 6;
|
|
95
|
+
const eyeH = char.eyes.h || char.eyes.size || 6;
|
|
96
|
+
const isKiro = char.name === 'kiro';
|
|
97
|
+
const effectColor = getEffectColor(char);
|
|
98
|
+
|
|
99
|
+
// Effect position (relative to character, above eyes)
|
|
100
|
+
const effectX = rightX + eyeW + 2;
|
|
101
|
+
const effectY = eyeY - 18;
|
|
102
|
+
|
|
103
|
+
switch (eyeType) {
|
|
104
|
+
case 'focused':
|
|
105
|
+
// Sunglasses for Matrix style (working state)
|
|
106
|
+
drawSunglasses(leftX, rightX, eyeY, eyeW, eyeH, drawRect, isKiro);
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'alert':
|
|
110
|
+
// Question mark effect (notification state)
|
|
111
|
+
drawQuestionMark(effectX, effectY, drawRect);
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case 'sparkle':
|
|
115
|
+
// Sparkle effect (start state)
|
|
116
|
+
drawSparkle(effectX, effectY + 2, animFrame, drawRect, effectColor);
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
case 'thinking':
|
|
120
|
+
// Thought bubble effect (thinking state)
|
|
121
|
+
drawThoughtBubble(effectX, effectY, animFrame, drawRect, effectColor);
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'sleep':
|
|
125
|
+
// Sleep eyes (closed eyes) and Zzz effect
|
|
126
|
+
drawSleepEyes(leftX, rightX, eyeY, eyeW, eyeH, drawRect, char.color, isKiro);
|
|
127
|
+
drawZzz(effectX, effectY, animFrame, drawRect, effectColor);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Draw sparkle (scaled 2x)
|
|
133
|
+
export function drawSparkle(x, y, animFrame, drawRect, color = COLOR_WHITE) {
|
|
134
|
+
const frame = animFrame % 4;
|
|
135
|
+
drawRect(x + 2, y + 2, 2, 2, color);
|
|
136
|
+
|
|
137
|
+
if (frame === 0 || frame === 2) {
|
|
138
|
+
drawRect(x + 2, y, 2, 2, color);
|
|
139
|
+
drawRect(x + 2, y + 4, 2, 2, color);
|
|
140
|
+
drawRect(x, y + 2, 2, 2, color);
|
|
141
|
+
drawRect(x + 4, y + 2, 2, 2, color);
|
|
142
|
+
} else {
|
|
143
|
+
drawRect(x, y, 2, 2, color);
|
|
144
|
+
drawRect(x + 4, y, 2, 2, color);
|
|
145
|
+
drawRect(x, y + 4, 2, 2, color);
|
|
146
|
+
drawRect(x + 4, y + 4, 2, 2, color);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Draw question mark
|
|
151
|
+
export function drawQuestionMark(x, y, drawRect) {
|
|
152
|
+
const color = '#000000';
|
|
153
|
+
drawRect(x + 1, y, 4, 2, color);
|
|
154
|
+
drawRect(x + 4, y + 2, 2, 2, color);
|
|
155
|
+
drawRect(x + 2, y + 4, 2, 2, color);
|
|
156
|
+
drawRect(x + 2, y + 6, 2, 2, color);
|
|
157
|
+
drawRect(x + 2, y + 10, 2, 2, color);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Draw Zzz animation for sleep state
|
|
161
|
+
export function drawZzz(x, y, animFrame, drawRect, color = COLOR_WHITE) {
|
|
162
|
+
const frame = animFrame % 20;
|
|
163
|
+
if (frame < 10) {
|
|
164
|
+
drawRect(x, y, 6, 1, color);
|
|
165
|
+
drawRect(x + 4, y + 1, 2, 1, color);
|
|
166
|
+
drawRect(x + 3, y + 2, 2, 1, color);
|
|
167
|
+
drawRect(x + 2, y + 3, 2, 1, color);
|
|
168
|
+
drawRect(x + 1, y + 4, 2, 1, color);
|
|
169
|
+
drawRect(x, y + 5, 6, 1, color);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Draw thought bubble animation for thinking state
|
|
174
|
+
export function drawThoughtBubble(x, y, animFrame, drawRect, color = COLOR_WHITE) {
|
|
175
|
+
const frame = animFrame % 12;
|
|
176
|
+
// Small dots leading to bubble (always visible)
|
|
177
|
+
drawRect(x, y + 6, 2, 2, color);
|
|
178
|
+
drawRect(x + 2, y + 3, 2, 2, color);
|
|
179
|
+
// Main bubble (animated size)
|
|
180
|
+
if (frame < 6) {
|
|
181
|
+
// Larger bubble
|
|
182
|
+
drawRect(x + 3, y - 2, 6, 2, color);
|
|
183
|
+
drawRect(x + 2, y, 8, 3, color);
|
|
184
|
+
drawRect(x + 3, y + 3, 6, 1, color);
|
|
185
|
+
} else {
|
|
186
|
+
// Smaller bubble
|
|
187
|
+
drawRect(x + 4, y - 1, 4, 2, color);
|
|
188
|
+
drawRect(x + 3, y + 1, 6, 2, color);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Matrix rain colors (green shades - movie style)
|
|
193
|
+
const COLOR_MATRIX_WHITE = '#CCFFCC';
|
|
194
|
+
const COLOR_MATRIX_BRIGHT = '#00FF00';
|
|
195
|
+
const COLOR_MATRIX_MID = '#00BB00';
|
|
196
|
+
const COLOR_MATRIX_DIM = '#008800';
|
|
197
|
+
const COLOR_MATRIX_DARK = '#004400';
|
|
198
|
+
|
|
199
|
+
// Pseudo-random number generator for consistent randomness
|
|
200
|
+
function pseudoRandom(seed) {
|
|
201
|
+
const x = Math.sin(seed * 9999) * 10000;
|
|
202
|
+
return x - Math.floor(x);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Draw matrix background effect (full area, movie style)
|
|
206
|
+
export function drawMatrixBackground(animFrame, drawRect, size = 64, body = null) {
|
|
207
|
+
// Draw streams across entire area (character will be drawn on top)
|
|
208
|
+
const streamCount = Math.floor(size / MATRIX_COLUMN_WIDTH);
|
|
209
|
+
for (let i = 0; i < streamCount; i++) {
|
|
210
|
+
const seed = i * 23 + 7;
|
|
211
|
+
// Show streams based on density setting
|
|
212
|
+
if (pseudoRandom(seed + 100) > MATRIX_STREAM_DENSITY) continue;
|
|
213
|
+
const x = i * MATRIX_COLUMN_WIDTH;
|
|
214
|
+
const offset = Math.floor(pseudoRandom(seed) * size);
|
|
215
|
+
// Variable speed
|
|
216
|
+
const speedRange = MATRIX_SPEED_MAX - MATRIX_SPEED_MIN + 1;
|
|
217
|
+
const speed = MATRIX_SPEED_MIN + Math.floor(pseudoRandom(seed + 1) * speedRange);
|
|
218
|
+
// Variable tail length based on speed
|
|
219
|
+
const tailLen = speed > 3 ? MATRIX_TAIL_LENGTH_FAST : MATRIX_TAIL_LENGTH_SLOW;
|
|
220
|
+
drawMatrixStreamMovie(x, 0, animFrame, drawRect, offset, size, speed, tailLen, seed);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Draw matrix stream with movie-style effect
|
|
225
|
+
function drawMatrixStreamMovie(x, y, animFrame, drawRect, offset, height, speed, tailLen, seed) {
|
|
226
|
+
if (height < MATRIX_COLUMN_WIDTH) return;
|
|
227
|
+
const pos = (animFrame * speed + offset) % height;
|
|
228
|
+
|
|
229
|
+
// Head: bright white/green (flicker effect)
|
|
230
|
+
const flicker = (animFrame + seed) % MATRIX_FLICKER_PERIOD === 0;
|
|
231
|
+
const headColor = flicker ? COLOR_MATRIX_WHITE : COLOR_MATRIX_BRIGHT;
|
|
232
|
+
drawRect(x, y + pos, 2, 2, headColor);
|
|
233
|
+
|
|
234
|
+
// Tail with gradient
|
|
235
|
+
if (pos >= 2) drawRect(x, y + pos - 2, 2, 2, COLOR_MATRIX_BRIGHT);
|
|
236
|
+
if (pos >= 4) drawRect(x, y + pos - 4, 2, 2, COLOR_MATRIX_MID);
|
|
237
|
+
if (pos >= 6) drawRect(x, y + pos - 6, 2, 2, COLOR_MATRIX_MID);
|
|
238
|
+
if (tailLen >= 8 && pos >= 8) drawRect(x, y + pos - 8, 2, 2, COLOR_MATRIX_DIM);
|
|
239
|
+
if (tailLen >= 8 && pos >= 10) drawRect(x, y + pos - 10, 2, 2, COLOR_MATRIX_DARK);
|
|
240
|
+
}
|
package/shared/icons.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Cached DOM elements and canvas contexts
|
|
2
|
+
let iconCache = null;
|
|
3
|
+
|
|
4
|
+
// Initialize icon cache (called once on first drawInfoIcons call)
|
|
5
|
+
function initIconCache() {
|
|
6
|
+
const iconProject = document.getElementById('icon-project');
|
|
7
|
+
const iconTool = document.getElementById('icon-tool');
|
|
8
|
+
const iconModel = document.getElementById('icon-model');
|
|
9
|
+
const iconMemory = document.getElementById('icon-memory');
|
|
10
|
+
|
|
11
|
+
iconCache = {
|
|
12
|
+
emojiIcons: document.querySelectorAll('.emoji-icon'),
|
|
13
|
+
pixelIcons: document.querySelectorAll('.pixel-icon'),
|
|
14
|
+
// Cache both canvas elements and their contexts
|
|
15
|
+
canvases: [
|
|
16
|
+
{ canvas: iconProject, ctx: iconProject?.getContext('2d') },
|
|
17
|
+
{ canvas: iconTool, ctx: iconTool?.getContext('2d') },
|
|
18
|
+
{ canvas: iconModel, ctx: iconModel?.getContext('2d') },
|
|
19
|
+
{ canvas: iconMemory, ctx: iconMemory?.getContext('2d') }
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Draw folder icon - 8x7 pixels
|
|
25
|
+
export function drawFolderIcon(iconCtx, color) {
|
|
26
|
+
iconCtx.fillStyle = color;
|
|
27
|
+
iconCtx.fillRect(0, 0, 3, 1);
|
|
28
|
+
iconCtx.fillRect(0, 1, 8, 6);
|
|
29
|
+
iconCtx.fillStyle = '#000000';
|
|
30
|
+
iconCtx.fillRect(1, 2, 6, 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Draw tool/wrench icon - 8x8 pixels
|
|
34
|
+
export function drawToolIcon(iconCtx, color) {
|
|
35
|
+
iconCtx.fillStyle = color;
|
|
36
|
+
iconCtx.fillRect(1, 0, 6, 3);
|
|
37
|
+
iconCtx.fillStyle = '#000000';
|
|
38
|
+
iconCtx.fillRect(3, 0, 2, 1);
|
|
39
|
+
iconCtx.fillStyle = color;
|
|
40
|
+
iconCtx.fillRect(3, 3, 2, 5);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Draw robot icon - 8x8 pixels
|
|
44
|
+
export function drawRobotIcon(iconCtx, color) {
|
|
45
|
+
iconCtx.fillStyle = color;
|
|
46
|
+
iconCtx.fillRect(3, 0, 2, 1);
|
|
47
|
+
iconCtx.fillRect(1, 1, 6, 5);
|
|
48
|
+
iconCtx.fillStyle = '#000000';
|
|
49
|
+
iconCtx.fillRect(2, 2, 1, 2);
|
|
50
|
+
iconCtx.fillRect(5, 2, 1, 2);
|
|
51
|
+
iconCtx.fillRect(2, 5, 4, 1);
|
|
52
|
+
iconCtx.fillStyle = color;
|
|
53
|
+
iconCtx.fillRect(0, 2, 1, 2);
|
|
54
|
+
iconCtx.fillRect(7, 2, 1, 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Draw brain icon - 8x7 pixels
|
|
58
|
+
export function drawBrainIcon(iconCtx, color) {
|
|
59
|
+
iconCtx.fillStyle = color;
|
|
60
|
+
iconCtx.fillRect(1, 0, 6, 7);
|
|
61
|
+
iconCtx.fillRect(0, 1, 8, 5);
|
|
62
|
+
iconCtx.fillStyle = '#000000';
|
|
63
|
+
iconCtx.fillRect(4, 1, 1, 5);
|
|
64
|
+
iconCtx.fillRect(2, 0, 1, 1);
|
|
65
|
+
iconCtx.fillRect(5, 0, 1, 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Draw all info icons
|
|
69
|
+
export function drawInfoIcons(color, bgColor, useEmoji) {
|
|
70
|
+
// Initialize cache on first call
|
|
71
|
+
if (!iconCache) {
|
|
72
|
+
initIconCache();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const c = iconCache;
|
|
76
|
+
|
|
77
|
+
// Toggle emoji/pixel icon visibility (using cached elements)
|
|
78
|
+
c.emojiIcons.forEach(el => el.style.display = useEmoji ? 'inline' : 'none');
|
|
79
|
+
c.pixelIcons.forEach(el => el.style.display = useEmoji ? 'none' : 'inline-block');
|
|
80
|
+
|
|
81
|
+
if (!useEmoji) {
|
|
82
|
+
const drawFuncs = [drawFolderIcon, drawToolIcon, drawRobotIcon, drawBrainIcon];
|
|
83
|
+
|
|
84
|
+
// Use cached contexts instead of calling getContext('2d') each time
|
|
85
|
+
c.canvases.forEach((item, index) => {
|
|
86
|
+
if (item.ctx) {
|
|
87
|
+
item.ctx.fillStyle = bgColor;
|
|
88
|
+
item.ctx.fillRect(0, 0, 8, 8);
|
|
89
|
+
drawFuncs[index](item.ctx, color);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* Display Screen */
|
|
2
|
+
.display {
|
|
3
|
+
width: 172px;
|
|
4
|
+
height: 320px;
|
|
5
|
+
background: #000;
|
|
6
|
+
border-radius: 4px;
|
|
7
|
+
position: relative;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
image-rendering: pixelated;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Character Canvas - 128x128, centered */
|
|
13
|
+
#character-canvas {
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 20px;
|
|
16
|
+
left: 22px;
|
|
17
|
+
width: 128px;
|
|
18
|
+
height: 128px;
|
|
19
|
+
image-rendering: pixelated;
|
|
20
|
+
transition: top 0.1s ease-out, left 0.1s ease-out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Status Text */
|
|
24
|
+
.status-text {
|
|
25
|
+
position: absolute;
|
|
26
|
+
top: 160px;
|
|
27
|
+
width: 100%;
|
|
28
|
+
text-align: center;
|
|
29
|
+
font-family: 'Courier New', monospace;
|
|
30
|
+
font-size: 24px;
|
|
31
|
+
font-weight: bold;
|
|
32
|
+
color: white;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Loading Dots */
|
|
36
|
+
.loading-dots {
|
|
37
|
+
position: absolute;
|
|
38
|
+
top: 190px;
|
|
39
|
+
width: 100%;
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dot {
|
|
46
|
+
width: 8px;
|
|
47
|
+
height: 8px;
|
|
48
|
+
border-radius: 50%;
|
|
49
|
+
background: #7BEF7B;
|
|
50
|
+
transition: background 0.1s;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dot.dim {
|
|
54
|
+
background: #3a3a3a;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Info Text */
|
|
58
|
+
.info-text {
|
|
59
|
+
position: absolute;
|
|
60
|
+
font-family: 'Courier New', monospace;
|
|
61
|
+
font-size: 10px;
|
|
62
|
+
color: white;
|
|
63
|
+
left: 10px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.project-text {
|
|
67
|
+
top: 220px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.tool-text {
|
|
71
|
+
top: 235px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.model-text {
|
|
75
|
+
top: 250px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.memory-text {
|
|
79
|
+
top: 265px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.memory-bar-container {
|
|
83
|
+
position: absolute;
|
|
84
|
+
top: 285px;
|
|
85
|
+
left: 10px;
|
|
86
|
+
width: 152px;
|
|
87
|
+
height: 8px;
|
|
88
|
+
background: rgba(0, 0, 0, 0.4);
|
|
89
|
+
border-radius: 2px;
|
|
90
|
+
border: 1px solid rgba(0, 0, 0, 0.6);
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.memory-bar {
|
|
95
|
+
height: 100%;
|
|
96
|
+
border-radius: 0px;
|
|
97
|
+
transition: width 0.3s, background 0.3s;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.info-label {
|
|
101
|
+
color: white;
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.info-value {
|
|
107
|
+
color: #aaa;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.pixel-icon {
|
|
111
|
+
width: 8px;
|
|
112
|
+
height: 8px;
|
|
113
|
+
margin-right: 2px;
|
|
114
|
+
image-rendering: pixelated;
|
|
115
|
+
vertical-align: middle;
|
|
116
|
+
display: none;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.emoji-icon {
|
|
120
|
+
display: inline;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.icon-canvas {
|
|
124
|
+
width: 8px;
|
|
125
|
+
height: 8px;
|
|
126
|
+
image-rendering: pixelated;
|
|
127
|
+
vertical-align: middle;
|
|
128
|
+
margin-right: 2px;
|
|
129
|
+
}
|
package/shared/utils.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { TOOL_TEXTS, THINKING_TEXTS } from './config.js';
|
|
2
|
+
|
|
3
|
+
// Get thinking text (random selection)
|
|
4
|
+
export function getThinkingText() {
|
|
5
|
+
return THINKING_TEXTS[Math.floor(Math.random() * THINKING_TEXTS.length)];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Get working text based on tool (random selection, case-insensitive)
|
|
9
|
+
export function getWorkingText(tool) {
|
|
10
|
+
const key = (tool || '').toLowerCase();
|
|
11
|
+
const texts = TOOL_TEXTS[key] || TOOL_TEXTS['default'];
|
|
12
|
+
return texts[Math.floor(Math.random() * texts.length)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Interpolate between two colors based on ratio (0-1)
|
|
16
|
+
export function lerpColor(color1, color2, ratio) {
|
|
17
|
+
const r1 = parseInt(color1.slice(1, 3), 16);
|
|
18
|
+
const g1 = parseInt(color1.slice(3, 5), 16);
|
|
19
|
+
const b1 = parseInt(color1.slice(5, 7), 16);
|
|
20
|
+
const r2 = parseInt(color2.slice(1, 3), 16);
|
|
21
|
+
const g2 = parseInt(color2.slice(3, 5), 16);
|
|
22
|
+
const b2 = parseInt(color2.slice(5, 7), 16);
|
|
23
|
+
|
|
24
|
+
const r = Math.round(r1 + (r2 - r1) * ratio);
|
|
25
|
+
const g = Math.round(g1 + (g2 - g1) * ratio);
|
|
26
|
+
const b = Math.round(b1 + (b2 - b1) * ratio);
|
|
27
|
+
|
|
28
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get gradient colors based on percentage (smooth transition)
|
|
32
|
+
export function getMemoryGradient(percent) {
|
|
33
|
+
const green = '#00AA00';
|
|
34
|
+
const yellow = '#FFCC00';
|
|
35
|
+
const red = '#FF4444';
|
|
36
|
+
|
|
37
|
+
let startColor, endColor;
|
|
38
|
+
if (percent < 50) {
|
|
39
|
+
const ratio = percent / 50;
|
|
40
|
+
startColor = lerpColor(green, yellow, ratio * 0.5);
|
|
41
|
+
endColor = lerpColor(green, yellow, Math.min(1, ratio * 0.5 + 0.3));
|
|
42
|
+
} else {
|
|
43
|
+
const ratio = (percent - 50) / 50;
|
|
44
|
+
startColor = lerpColor(yellow, red, ratio * 0.7);
|
|
45
|
+
endColor = lerpColor(yellow, red, Math.min(1, ratio * 0.7 + 0.3));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return `linear-gradient(to right, ${startColor}, ${endColor})`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Update memory bar display
|
|
52
|
+
export function updateMemoryBar(memoryUsage, bgColor) {
|
|
53
|
+
const memoryBar = document.getElementById('memory-bar');
|
|
54
|
+
const memoryBarContainer = document.getElementById('memory-bar-container');
|
|
55
|
+
|
|
56
|
+
if (!memoryUsage || memoryUsage === '-') {
|
|
57
|
+
memoryBarContainer.style.display = 'none';
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
memoryBarContainer.style.display = 'block';
|
|
62
|
+
|
|
63
|
+
const isDarkBg = (bgColor === '#0066CC' || bgColor === '#1a1a4e');
|
|
64
|
+
if (isDarkBg) {
|
|
65
|
+
memoryBarContainer.style.borderColor = 'rgba(255, 255, 255, 0.6)';
|
|
66
|
+
memoryBarContainer.style.background = 'rgba(255, 255, 255, 0.2)';
|
|
67
|
+
} else {
|
|
68
|
+
memoryBarContainer.style.borderColor = 'rgba(0, 0, 0, 0.6)';
|
|
69
|
+
memoryBarContainer.style.background = 'rgba(0, 0, 0, 0.3)';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const percent = parseInt(memoryUsage.replace('%', '')) || 0;
|
|
73
|
+
const clampedPercent = Math.min(100, Math.max(0, percent));
|
|
74
|
+
|
|
75
|
+
memoryBar.style.width = clampedPercent + '%';
|
|
76
|
+
memoryBar.style.background = getMemoryGradient(clampedPercent);
|
|
77
|
+
}
|
package/styles.css
CHANGED
|
@@ -64,3 +64,14 @@ html, body {
|
|
|
64
64
|
border-radius: 0 0 12px 12px;
|
|
65
65
|
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/* Version text */
|
|
69
|
+
.version-text {
|
|
70
|
+
position: absolute;
|
|
71
|
+
bottom: 4px;
|
|
72
|
+
width: 100%;
|
|
73
|
+
text-align: center;
|
|
74
|
+
font-size: 9px;
|
|
75
|
+
color: #666;
|
|
76
|
+
font-family: monospace;
|
|
77
|
+
}
|