vibe-monitor 1.0.1 → 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
@@ -18,6 +18,16 @@ AI coding assistant status monitor with pixel art character.
18
18
  npx vibe-monitor
19
19
  ```
20
20
 
21
+ ### Stop
22
+
23
+ ```bash
24
+ curl -X POST http://127.0.0.1:19280/quit
25
+
26
+ # or
27
+ pkill -f vibe-monitor
28
+ killall Electron
29
+ ```
30
+
21
31
  ## Installation
22
32
 
23
33
  ### From npm
@@ -135,6 +145,14 @@ Show window:
135
145
  curl -X POST http://127.0.0.1:19280/show
136
146
  ```
137
147
 
148
+ ### POST /quit
149
+
150
+ Quit application:
151
+
152
+ ```bash
153
+ curl -X POST http://127.0.0.1:19280/quit
154
+ ```
155
+
138
156
  ## Tray Menu
139
157
 
140
158
  Click the system tray icon to:
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,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-monitor",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "AI coding assistant status monitor",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -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
+ }