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 CHANGED
@@ -1,6 +1,8 @@
1
- # Vibe Monitor Desktop App
1
+ # Vibe Monitor
2
2
 
3
- Electron-based desktop app for real-time monitoring of AI coding assistants (Claude Code, Kiro IDE/CLI) with pixel art character.
3
+ AI coding assistant status monitor with pixel art character.
4
+
5
+ ![Demo](assets/demo.gif)
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
- ## Installation
15
+ ## Quick Start
14
16
 
15
17
  ```bash
16
- cd desktop
17
- npm install
18
+ npx vibe-monitor
18
19
  ```
19
20
 
20
- ## Usage
21
-
22
- ### Run the App
21
+ ### Stop
23
22
 
24
23
  ```bash
25
- npm start
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
- # Check current status
37
- curl http://127.0.0.1:19280/status
26
+ # or
27
+ pkill -f vibe-monitor
28
+ killall Electron
38
29
  ```
39
30
 
40
- ### IDE Hooks Integration
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
- Copy hook files to `~/.kiro/hooks/` (or your project's `.kiro/hooks/` folder):
33
+ ### From npm
49
34
 
50
35
  ```bash
51
- # Global installation (recommended)
52
- mkdir -p ~/.kiro/hooks
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
- **Hook files:**
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
- ## Supported IDEs
66
-
67
- | IDE | Hook System | Status |
68
- |-----|-------------|--------|
69
- | **Claude Code** | Shell hooks via `settings.json` | ✅ Supported |
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
- ### GET /status
49
+ ## States
95
50
 
96
- Get current status
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
- ```json
99
- {
100
- "state": "working",
101
- "project": "vibe-monitor",
102
- "tool": "Bash",
103
- "model": "opus",
104
- "memory": "45%"
105
- }
106
- ```
61
+ ## Characters
107
62
 
108
- ### GET /health
63
+ - **clawd** (default): Orange pixel art character
64
+ - **kiro**: White ghost character
109
65
 
110
- Health check endpoint
66
+ ## IDE Integration
111
67
 
112
- ```json
113
- {
114
- "status": "ok"
115
- }
116
- ```
68
+ ### Claude Code
117
69
 
118
- ### POST /show
119
-
120
- Show window and position to top-right corner
70
+ Add to `~/.claude/settings.json`:
121
71
 
122
72
  ```json
123
73
  {
124
- "success": true
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
- ### GET /debug
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
- ### Install Dependencies
92
+ ### Kiro IDE
157
93
 
158
- Electron requires several system libraries that are not installed by default on WSL:
94
+ Copy hook files from the repository to `~/.kiro/hooks/`:
159
95
 
160
96
  ```bash
161
- # Ubuntu 24.04 (Noble) or later
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
- ### Run
197
-
198
- ```bash
199
- cd desktop
200
- npm install
201
- npm start
202
- ```
100
+ ## API
203
101
 
204
- ### Troubleshooting
102
+ ### POST /status
205
103
 
206
- **Error: `libasound.so.2: cannot open shared object file`**
104
+ Update status:
207
105
 
208
- Install the audio library:
209
106
  ```bash
210
- # Ubuntu 24.04+
211
- sudo apt-get install -y libasound2t64
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
- **GPU process errors (can be ignored)**
112
+ **Fields:**
218
113
 
219
- WSL may show GPU-related warnings like:
220
- ```
221
- Exiting GPU process due to errors during initialization
222
- ```
223
- These warnings don't affect app functionality.
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
- **Window not appearing**
126
+ Get current status:
226
127
 
227
- Ensure WSLg is working:
228
128
  ```bash
229
- # Test with a simple GUI app
230
- sudo apt-get install -y x11-apps
231
- xclock
129
+ curl http://127.0.0.1:19280/status
232
130
  ```
233
131
 
234
- If xclock doesn't appear, WSLg may need to be enabled or updated.
235
-
236
- ## Build
132
+ ### GET /health
237
133
 
238
- Build for macOS:
134
+ Health check:
239
135
 
240
136
  ```bash
241
- npm run build:mac
137
+ curl http://127.0.0.1:19280/health
242
138
  ```
243
139
 
244
- Build DMG only:
245
-
246
- ```bash
247
- npm run build:dmg
248
- ```
140
+ ### POST /show
249
141
 
250
- Build for Windows:
142
+ Show window:
251
143
 
252
144
  ```bash
253
- npm run build:win
145
+ curl -X POST http://127.0.0.1:19280/show
254
146
  ```
255
147
 
256
- Build for Linux:
148
+ ### POST /quit
257
149
 
258
- ```bash
259
- npm run build:linux
260
- ```
261
-
262
- Build for all platforms:
150
+ Quit application:
263
151
 
264
152
  ```bash
265
- npm run build:all
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
- (Can be changed via `HTTP_PORT` constant in main.js)
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
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
- stdio: 'inherit'
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="../shared/styles.css">
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.0",
4
- "description": "Claude Code Status Monitor - Frameless Desktop App",
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 '../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';
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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }