agentgui 1.0.525 → 1.0.527

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/lib/speech.js CHANGED
@@ -1,4 +1,51 @@
1
1
  import { createRequire } from 'module';
2
2
  const require = createRequire(import.meta.url);
3
3
  const speech = require('webtalk/speech');
4
- export const { transcribe, synthesize, synthesizeStream, getSTT, getStatus, getVoices, preloadTTS, ttsCacheKey, ttsCacheGet, splitSentences, resetSTTError, clearCorruptedSTTCache } = speech;
4
+
5
+ const ttsMemCache = new Map();
6
+ const TTS_CACHE_MAX_BYTES = 10 * 1024 * 1024;
7
+ let ttsCacheBytes = 0;
8
+
9
+ function ttsCacheKey(text, voiceId) {
10
+ return (voiceId || 'default') + ':' + text;
11
+ }
12
+
13
+ function ttsCacheGet(key) {
14
+ return ttsMemCache.get(key) || null;
15
+ }
16
+
17
+ function ttsCacheSet(key, wav) {
18
+ if (ttsMemCache.has(key)) return;
19
+ const size = wav ? wav.length : 0;
20
+ while (ttsCacheBytes + size > TTS_CACHE_MAX_BYTES && ttsMemCache.size > 0) {
21
+ const oldest = ttsMemCache.keys().next().value;
22
+ const old = ttsMemCache.get(oldest);
23
+ ttsCacheBytes -= old ? old.length : 0;
24
+ ttsMemCache.delete(oldest);
25
+ }
26
+ ttsMemCache.set(key, wav);
27
+ ttsCacheBytes += size;
28
+ }
29
+
30
+ async function synthesizeWithCache(text, voiceId) {
31
+ const key = ttsCacheKey(text, voiceId);
32
+ const cached = ttsCacheGet(key);
33
+ if (cached) return cached;
34
+ const wav = await speech.synthesize(text, voiceId);
35
+ ttsCacheSet(key, wav);
36
+ return wav;
37
+ }
38
+
39
+ export const transcribe = speech.transcribe;
40
+ export const synthesize = synthesizeWithCache;
41
+ export const synthesizeStream = speech.synthesizeStream;
42
+ export const getSTT = speech.getSTT;
43
+ export const getStatus = speech.getStatus;
44
+ export const getVoices = speech.getVoices;
45
+ export const preloadTTS = speech.preloadTTS;
46
+ export { ttsCacheKey, ttsCacheGet, ttsCacheSet };
47
+ export const splitSentences = speech.splitSentences;
48
+ export const resetSTTError = speech.resetSTTError;
49
+ export const clearCorruptedSTTCache = speech.clearCorruptedSTTCache;
50
+ export const getSttOptions = speech.getSttOptions;
51
+ export const VOICE_DIRS = speech.VOICE_DIRS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.525",
3
+ "version": "1.0.527",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -64,7 +64,7 @@ const voiceCacheManager = {
64
64
  this.generating.set(cacheKey, true);
65
65
  try {
66
66
  const speech = await getSpeech();
67
- const audioBlob = await speech.ttsSync(text, 'en-US');
67
+ const audioBlob = await speech.synthesize(text, 'default');
68
68
  const saved = queries.saveVoiceCache(conversationId, text, audioBlob);
69
69
  const totalSize = queries.getVoiceCacheSize(conversationId);
70
70
  if (totalSize > this.maxCacheSize) {
@@ -240,14 +240,15 @@ function flushTTSaccumulator(key, conversationId, sessionId) {
240
240
  }
241
241
  }
242
242
  if (voices.size === 0) return;
243
- const cacheKey = speech.ttsCacheKey(text, vid);
244
243
  for (const vid of voices) {
244
+ const cacheKey = speech.ttsCacheKey(text, vid);
245
245
  const cached = speech.ttsCacheGet(cacheKey);
246
246
  if (cached) {
247
247
  pushTTSAudio(cacheKey, cached, conversationId, sessionId, vid);
248
248
  continue;
249
249
  }
250
250
  speech.synthesize(text, vid).then(wav => {
251
+ if (speech.ttsCacheSet) speech.ttsCacheSet(cacheKey, wav);
251
252
  pushTTSAudio(cacheKey, wav, conversationId, sessionId, vid);
252
253
  }).catch(() => {});
253
254
  }
@@ -2880,6 +2881,23 @@ const server = http.createServer(async (req, res) => {
2880
2881
  return;
2881
2882
  }
2882
2883
 
2884
+ if (pathOnly.startsWith('/api/tts-cache/') && req.method === 'GET') {
2885
+ const cacheKey = decodeURIComponent(pathOnly.slice('/api/tts-cache/'.length));
2886
+ try {
2887
+ const speech = await getSpeech();
2888
+ const cached = speech.ttsCacheGet(cacheKey);
2889
+ if (cached) {
2890
+ res.writeHead(200, { 'Content-Type': 'audio/wav', 'Content-Length': cached.length, 'Cache-Control': 'public, max-age=3600' });
2891
+ res.end(cached);
2892
+ } else {
2893
+ sendJSON(req, res, 404, { error: 'not cached' });
2894
+ }
2895
+ } catch (err) {
2896
+ sendJSON(req, res, 500, { error: err.message });
2897
+ }
2898
+ return;
2899
+ }
2900
+
2883
2901
  if (pathOnly === '/api/speech-status' && req.method === 'GET') {
2884
2902
  try {
2885
2903
  const { getStatus } = await getSpeech();
@@ -40,7 +40,7 @@
40
40
  background: var(--color-bg-secondary);
41
41
  border-radius: 1rem;
42
42
  width: 90%;
43
- max-width: 32rem;
43
+ max-width: 600px;
44
44
  max-height: 80vh;
45
45
  display: flex;
46
46
  flex-direction: column;
@@ -49,21 +49,91 @@
49
49
  font-family: system-ui, -apple-system, sans-serif;
50
50
  }
51
51
 
52
+ @media (max-width: 768px) {
53
+ .tools-popup-content {
54
+ max-width: 90vw;
55
+ width: 100%;
56
+ }
57
+ }
58
+
59
+ @media (max-width: 480px) {
60
+ .tools-popup-content {
61
+ max-width: 95vw;
62
+ width: 100%;
63
+ max-height: 85vh;
64
+ }
65
+
66
+ .tools-popup-header {
67
+ flex-direction: column;
68
+ align-items: flex-start;
69
+ gap: 0.5rem;
70
+ }
71
+
72
+ .tools-popup-header-controls {
73
+ width: 100%;
74
+ flex-wrap: wrap;
75
+ }
76
+ }
77
+
52
78
  .tools-popup-header {
53
79
  display: flex;
54
80
  justify-content: space-between;
55
81
  align-items: center;
56
- padding: 1.25rem 1.5rem;
82
+ padding: 0.875rem 1.25rem;
57
83
  flex-shrink: 0;
58
84
  border-bottom: 1px solid var(--color-border);
85
+ gap: 0.75rem;
59
86
  }
60
87
 
61
88
  .tools-popup-header h2 {
62
89
  margin: 0;
63
- font-size: 1.25rem;
90
+ font-size: 1rem;
64
91
  font-weight: 700;
65
92
  }
66
93
 
94
+ .tools-popup-header-controls {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 0.75rem;
98
+ flex-shrink: 0;
99
+ }
100
+
101
+ .tools-voice-toggle {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 0.5rem;
105
+ font-size: 0.8rem;
106
+ cursor: pointer;
107
+ user-select: none;
108
+ }
109
+
110
+ .tools-voice-toggle input {
111
+ width: 0.875rem;
112
+ height: 0.875rem;
113
+ cursor: pointer;
114
+ }
115
+
116
+ .tools-voice-selector {
117
+ padding: 0.35rem 0.5rem;
118
+ font-size: 0.75rem;
119
+ border-radius: 0.3rem;
120
+ border: 1px solid var(--color-border);
121
+ background: var(--color-bg-primary);
122
+ color: var(--color-text-primary);
123
+ cursor: pointer;
124
+ transition: border-color 0.2s;
125
+ }
126
+
127
+ .tools-voice-selector:hover {
128
+ border-color: var(--color-primary);
129
+ }
130
+
131
+ .tools-voice-selector:focus {
132
+ outline: none;
133
+ border-color: var(--color-primary);
134
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
135
+ }
136
+
67
137
  .tools-popup-close {
68
138
  background: none;
69
139
  border: none;
@@ -83,9 +153,21 @@
83
153
  flex: 1;
84
154
  overflow-y: auto;
85
155
  padding: 1rem;
86
- display: flex;
87
- flex-direction: column;
88
- gap: 0.75rem;
156
+ display: grid;
157
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
158
+ gap: 1rem;
159
+ }
160
+
161
+ @media (max-width: 768px) {
162
+ .tools-popup-scroll {
163
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
164
+ }
165
+ }
166
+
167
+ @media (max-width: 480px) {
168
+ .tools-popup-scroll {
169
+ grid-template-columns: 1fr;
170
+ }
89
171
  }
90
172
 
91
173
  .tools-popup-scroll::-webkit-scrollbar {
@@ -106,43 +188,50 @@
106
188
  }
107
189
 
108
190
  .tool-item {
109
- padding: 1rem;
191
+ padding: 0.75rem;
110
192
  border: 1px solid var(--color-border);
111
193
  border-radius: 0.5rem;
112
194
  background: var(--color-bg-primary);
113
195
  display: flex;
114
196
  flex-direction: column;
115
- gap: 0.5rem;
197
+ gap: 0.375rem;
116
198
  transition: border-color 0.2s, background-color 0.2s;
199
+ min-height: 120px;
200
+ justify-content: space-between;
117
201
  }
118
202
 
119
203
  .tool-item:hover {
120
204
  border-color: var(--color-primary);
121
205
  background: var(--color-bg-secondary);
206
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
122
207
  }
123
208
 
124
209
  .tool-header {
125
210
  display: flex;
126
- align-items: center;
127
- gap: 0.75rem;
211
+ align-items: flex-start;
212
+ gap: 0.5rem;
128
213
  justify-content: space-between;
214
+ flex-wrap: wrap;
129
215
  }
130
216
 
131
217
  .tool-name {
132
218
  font-weight: 600;
133
- font-size: 0.95rem;
219
+ font-size: 0.9rem;
134
220
  flex: 1;
221
+ word-break: break-word;
135
222
  }
136
223
 
137
224
  .tool-status-indicator {
138
225
  display: inline-flex;
139
226
  align-items: center;
140
- gap: 0.375rem;
141
- font-size: 0.8rem;
142
- padding: 0.25rem 0.5rem;
227
+ gap: 0.25rem;
228
+ font-size: 0.7rem;
229
+ padding: 0.2rem 0.4rem;
143
230
  border-radius: 0.25rem;
144
231
  background: var(--color-bg-tertiary);
145
232
  color: var(--color-text-secondary);
233
+ white-space: nowrap;
234
+ flex-shrink: 0;
146
235
  }
147
236
 
148
237
  .tool-status-indicator.installed {
@@ -179,24 +268,28 @@
179
268
  }
180
269
 
181
270
  .tool-details {
182
- font-size: 0.8rem;
271
+ font-size: 0.7rem;
183
272
  color: var(--color-text-secondary);
184
- display: flex;
273
+ display: none;
185
274
  align-items: center;
186
275
  gap: 0.5rem;
276
+ line-height: 1.3;
277
+ max-height: 2.6em;
278
+ overflow: hidden;
187
279
  }
188
280
 
189
281
  .tool-progress-container {
190
282
  display: flex;
191
283
  flex-direction: column;
192
- gap: 0.25rem;
284
+ gap: 0.15rem;
285
+ margin-top: 0.25rem;
193
286
  }
194
287
 
195
288
  .tool-progress-bar {
196
289
  width: 100%;
197
- height: 0.375rem;
290
+ height: 0.25rem;
198
291
  background: var(--color-bg-tertiary);
199
- border-radius: 0.1875rem;
292
+ border-radius: 0.125rem;
200
293
  overflow: hidden;
201
294
  }
202
295
 
@@ -205,30 +298,31 @@
205
298
  background: linear-gradient(90deg, #3b82f6, #2563eb);
206
299
  width: 0%;
207
300
  transition: width 0.3s ease;
208
- border-radius: 0.1875rem;
301
+ border-radius: 0.125rem;
209
302
  }
210
303
 
211
304
  .tool-progress-text {
212
- font-size: 0.75rem;
305
+ font-size: 0.65rem;
213
306
  color: var(--color-text-secondary);
214
307
  }
215
308
 
216
309
  .tool-actions {
217
310
  display: flex;
218
- gap: 0.5rem;
311
+ gap: 0.4rem;
219
312
  flex-wrap: wrap;
313
+ margin-top: 0.25rem;
220
314
  }
221
315
 
222
316
  .tool-btn {
223
- padding: 0.5rem 0.875rem;
317
+ padding: 0.35rem 0.65rem;
224
318
  border: none;
225
- border-radius: 0.375rem;
319
+ border-radius: 0.3rem;
226
320
  cursor: pointer;
227
- font-size: 0.8rem;
321
+ font-size: 0.7rem;
228
322
  font-weight: 600;
229
323
  transition: all 0.2s;
230
324
  flex: 1;
231
- min-width: fit-content;
325
+ min-width: 60px;
232
326
  white-space: nowrap;
233
327
  }
234
328
 
@@ -259,12 +353,15 @@
259
353
  }
260
354
 
261
355
  .tool-error-message {
262
- font-size: 0.75rem;
356
+ font-size: 0.65rem;
263
357
  color: #ef4444;
264
358
  background: rgba(239, 68, 68, 0.1);
265
- padding: 0.5rem;
359
+ padding: 0.375rem;
266
360
  border-radius: 0.25rem;
267
361
  border-left: 2px solid #ef4444;
362
+ overflow: hidden;
363
+ text-overflow: ellipsis;
364
+ max-height: 1.5em;
268
365
  }
269
366
 
270
367
  .tools-popup-footer {
@@ -318,18 +415,21 @@
318
415
  .tool-versions {
319
416
  display: flex;
320
417
  flex-direction: column;
321
- gap: 0.25rem;
322
- font-size: 0.75rem;
418
+ gap: 0.15rem;
419
+ font-size: 0.65rem;
323
420
  color: var(--color-text-secondary);
324
- background: var(--color-bg-secondary);
325
- padding: 0.5rem;
421
+ background: transparent;
422
+ padding: 0;
326
423
  border-radius: 0.25rem;
424
+ line-height: 1.3;
327
425
  }
328
426
 
329
427
  .tool-version-item {
330
428
  display: flex;
331
429
  align-items: center;
332
- gap: 0.375rem;
430
+ gap: 0.25rem;
431
+ overflow: hidden;
432
+ text-overflow: ellipsis;
333
433
  }
334
434
 
335
435
  .tool-version-item strong {
package/static/index.html CHANGED
@@ -3125,8 +3125,19 @@
3125
3125
  <div class="tools-popup" id="toolsPopup">
3126
3126
  <div class="tools-popup-content" onclick="event.stopPropagation()">
3127
3127
  <div class="tools-popup-header">
3128
- <h2>Tools & Extensions</h2>
3129
- <button class="tools-popup-close" onclick="document.getElementById('toolsPopup').classList.remove('open')">×</button>
3128
+ <div style="display: flex; align-items: center; gap: 0.75rem; flex: 1;">
3129
+ <h2 style="margin: 0; font-size: 1rem;">Tools & Extensions</h2>
3130
+ </div>
3131
+ <div class="tools-popup-header-controls" style="display: flex; align-items: center; gap: 0.75rem; flex-shrink: 0;">
3132
+ <label class="tools-voice-toggle" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; cursor: pointer; user-select: none;">
3133
+ <input type="checkbox" id="toolsAutoSpeakToggle" style="width: 1rem; height: 1rem; cursor: pointer;">
3134
+ <span>Auto-speak</span>
3135
+ </label>
3136
+ <select class="tools-voice-selector" id="toolsVoiceSelector" style="padding: 0.4rem 0.6rem; font-size: 0.8rem; border-radius: 0.3rem; border: 1px solid var(--color-border); background: var(--color-bg-primary); color: var(--color-text-primary); cursor: pointer;">
3137
+ <option value="default">Voice</option>
3138
+ </select>
3139
+ <button class="tools-popup-close" onclick="document.getElementById('toolsPopup').classList.remove('open')" style="background: none; border: none; color: var(--color-text-secondary); font-size: 1.5rem; cursor: pointer; padding: 0; line-height: 1; transition: color 0.2s;">×</button>
3140
+ </div>
3130
3141
  </div>
3131
3142
  <div class="tools-popup-scroll"></div>
3132
3143
  <div class="tools-popup-footer">
@@ -13,9 +13,96 @@
13
13
  if (!btn.contains(e.target) && !popup.contains(e.target)) closePopup();
14
14
  });
15
15
  window.addEventListener('ws-message', onWsMessage);
16
+
17
+ // Initialize voice controls
18
+ initVoiceControls();
16
19
  refresh();
17
20
  }
18
21
 
22
+ function initVoiceControls() {
23
+ var autoSpeakToggle = document.getElementById('toolsAutoSpeakToggle');
24
+ var voiceSelector = document.getElementById('toolsVoiceSelector');
25
+
26
+ if (!autoSpeakToggle || !voiceSelector) return;
27
+
28
+ var savedAutoSpeak = localStorage.getItem('toolsAutoSpeak') === 'true';
29
+ autoSpeakToggle.checked = savedAutoSpeak;
30
+
31
+ window.addEventListener('ws-message', function(e) {
32
+ var data = e.detail;
33
+ if (data && data.type === 'voice_list') updateVoiceSelector(data.voices);
34
+ });
35
+
36
+ function trySubscribeVsManager() {
37
+ if (window.wsManager && window.wsManager.subscribeToVoiceList) {
38
+ window.wsManager.subscribeToVoiceList(updateVoiceSelector);
39
+ } else {
40
+ var BASE = window.__BASE_URL || '';
41
+ fetch(BASE + '/api/voices').then(function(r) { return r.json(); }).then(function(d) {
42
+ if (d.ok && Array.isArray(d.voices)) updateVoiceSelector(d.voices);
43
+ }).catch(function() {});
44
+ setTimeout(function() {
45
+ if (window.wsManager && window.wsManager.subscribeToVoiceList) {
46
+ window.wsManager.subscribeToVoiceList(updateVoiceSelector);
47
+ }
48
+ }, 2000);
49
+ }
50
+ }
51
+ trySubscribeVsManager();
52
+
53
+ autoSpeakToggle.addEventListener('change', function() {
54
+ localStorage.setItem('toolsAutoSpeak', this.checked);
55
+ if (window.voiceModule) window.voiceModule.setAutoSpeak(this.checked);
56
+ });
57
+
58
+ voiceSelector.addEventListener('change', function() {
59
+ localStorage.setItem('toolsVoice', this.value);
60
+ if (window.voiceModule) window.voiceModule.setVoice(this.value);
61
+ });
62
+ }
63
+
64
+ function updateVoiceSelector(voices) {
65
+ var voiceSelector = document.getElementById('toolsVoiceSelector');
66
+ if (!voiceSelector || !voices || !Array.isArray(voices)) return;
67
+
68
+ var currentValue = voiceSelector.value || localStorage.getItem('toolsVoice') || 'default';
69
+ voiceSelector.innerHTML = '';
70
+
71
+ var builtIn = voices.filter(function(v) { return !v.isCustom; });
72
+ var custom = voices.filter(function(v) { return v.isCustom; });
73
+
74
+ if (builtIn.length) {
75
+ var grp1 = document.createElement('optgroup');
76
+ grp1.label = 'Built-in Voices';
77
+ builtIn.forEach(function(voice) {
78
+ var opt = document.createElement('option');
79
+ opt.value = voice.id;
80
+ var parts = [];
81
+ if (voice.gender && voice.gender !== 'custom') parts.push(voice.gender);
82
+ if (voice.accent && voice.accent !== 'custom') parts.push(voice.accent);
83
+ opt.textContent = voice.name + (parts.length ? ' (' + parts.join(', ') + ')' : '');
84
+ grp1.appendChild(opt);
85
+ });
86
+ voiceSelector.appendChild(grp1);
87
+ }
88
+
89
+ if (custom.length) {
90
+ var grp2 = document.createElement('optgroup');
91
+ grp2.label = 'Custom Voices';
92
+ custom.forEach(function(voice) {
93
+ var opt = document.createElement('option');
94
+ opt.value = voice.id;
95
+ opt.textContent = voice.name;
96
+ grp2.appendChild(opt);
97
+ });
98
+ voiceSelector.appendChild(grp2);
99
+ }
100
+
101
+ if (voiceSelector.querySelector('option[value="' + currentValue + '"]')) {
102
+ voiceSelector.value = currentValue;
103
+ }
104
+ }
105
+
19
106
  function refresh() {
20
107
  fetchTools();
21
108
  }
@@ -141,10 +228,23 @@
141
228
  popup.classList.remove('open');
142
229
  }
143
230
 
231
+ function isAutoSpeakOn() {
232
+ var toggle = document.getElementById('toolsAutoSpeakToggle');
233
+ return toggle ? toggle.checked : false;
234
+ }
235
+
144
236
  function onWsMessage(e) {
145
237
  var data = e.detail;
146
238
  if (!data) return;
147
239
 
240
+ if (data.type === 'streaming_progress' && data.block && data.block.type === 'text' && data.block.text) {
241
+ if (isAutoSpeakOn() && (!data.blockRole || data.blockRole === 'assistant')) {
242
+ if (window.voiceModule && typeof window.voiceModule.speakText === 'function') {
243
+ window.voiceModule.speakText(data.block.text);
244
+ }
245
+ }
246
+ }
247
+
148
248
  if (data.type === 'tools_update_started') {
149
249
  var updateTools = data.tools || [];
150
250
  updateTools.forEach(function(toolId) {
@@ -200,42 +300,41 @@
200
300
  if (!scroll) return;
201
301
 
202
302
  if (tools.length === 0) {
203
- scroll.innerHTML = '<div class="tool-empty-state"><div class="tool-empty-state-icon">⚙️</div><div class="tool-empty-state-text">No tools available</div></div>';
303
+ scroll.innerHTML = '<div class="tool-empty-state" style="grid-column: 1 / -1;"><div class="tool-empty-state-icon">⚙️</div><div class="tool-empty-state-text">No tools available</div></div>';
204
304
  return;
205
305
  }
206
306
 
207
307
  scroll.innerHTML = tools.map(function(tool) {
208
308
  var statusClass = getStatusClass(tool);
209
309
  var isInstalling = tool.status === 'installing' || tool.status === 'updating';
210
- var hasAction = !tool.installed || tool.hasUpdate || tool.status === 'failed';
211
310
  var versionInfo = '';
212
311
  if (tool.installedVersion || tool.publishedVersion) {
213
312
  versionInfo = '<div class="tool-versions">';
214
313
  if (tool.installedVersion) {
215
- versionInfo += '<span class="tool-version-item">Installed: <strong>' + esc(tool.installedVersion) + '</strong></span>';
314
+ versionInfo += '<span class="tool-version-item">v' + esc(tool.installedVersion) + '</span>';
216
315
  }
217
- if (tool.publishedVersion) {
218
- versionInfo += '<span class="tool-version-item">Published: <strong>' + esc(tool.publishedVersion) + '</strong></span>';
316
+ if (tool.publishedVersion && tool.installedVersion !== tool.publishedVersion) {
317
+ versionInfo += '<span class="tool-version-item">(v' + esc(tool.publishedVersion) + ' available)</span>';
219
318
  }
220
319
  versionInfo += '</div>';
221
320
  }
222
321
 
223
322
  return '<div class="tool-item">' +
323
+ '<div style="display: flex; flex-direction: column; gap: 0.3rem;">' +
224
324
  '<div class="tool-header">' +
225
325
  '<span class="tool-name">' + esc(tool.id) + '</span>' +
226
- '<span class="tool-status-indicator ' + statusClass + '">' +
326
+ '</div>' +
327
+ '<div class="tool-status-indicator ' + statusClass + '">' +
227
328
  '<span class="tool-status-dot"></span>' +
228
329
  '<span>' + getStatusText(tool) + '</span>' +
229
- '</span>' +
230
330
  '</div>' +
231
331
  versionInfo +
232
- (tool.description ? '<div class="tool-details">' + esc(tool.description) + '</div>' : '') +
233
332
  (isInstalling && tool.progress !== undefined ?
234
333
  '<div class="tool-progress-container">' +
235
334
  '<div class="tool-progress-bar"><div class="tool-progress-fill" style="width:' + Math.min(tool.progress, 100) + '%"></div></div>' +
236
- '<div class="tool-progress-text">' + Math.floor(tool.progress) + '%</div>' +
237
335
  '</div>' : '') +
238
- (tool.error_message ? '<div class="tool-error-message">Error: ' + esc(tool.error_message.substring(0, 60)) + '</div>' : '') +
336
+ (tool.error_message ? '<div class="tool-error-message">Error: ' + esc(tool.error_message.substring(0, 40)) + '</div>' : '') +
337
+ '</div>' +
239
338
  '<div class="tool-actions">' +
240
339
  (tool.status === 'not_installed' ?
241
340
  '<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.install(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Install</button>' :
@@ -243,7 +342,7 @@
243
342
  '<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.update(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Update</button>' :
244
343
  tool.status === 'failed' ?
245
344
  '<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.install(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Retry</button>' :
246
- '<button class="tool-btn tool-btn-secondary" onclick="window.toolsManager.refresh()" ' + (isRefreshing ? 'disabled' : '') + '>Refresh</button>'
345
+ '<button class="tool-btn tool-btn-secondary" onclick="window.toolsManager.refresh()" ' + (isRefreshing ? 'disabled' : '') + '>✓</button>'
247
346
  ) +
248
347
  '</div>' +
249
348
  '</div>';
@@ -306,6 +405,28 @@
306
405
  },
307
406
  install: function(toolId) { install(toolId); },
308
407
  update: function(toolId) { update(toolId); },
309
- updateAll: function() { updateAll(); }
408
+ updateAll: function() { updateAll(); },
409
+ getAutoSpeak: function() {
410
+ var toggle = document.getElementById('toolsAutoSpeakToggle');
411
+ return toggle ? toggle.checked : false;
412
+ },
413
+ getVoice: function() {
414
+ var selector = document.getElementById('toolsVoiceSelector');
415
+ return selector ? selector.value : 'default';
416
+ },
417
+ setAutoSpeak: function(value) {
418
+ var toggle = document.getElementById('toolsAutoSpeakToggle');
419
+ if (toggle) {
420
+ toggle.checked = value;
421
+ localStorage.setItem('toolsAutoSpeak', value);
422
+ }
423
+ },
424
+ setVoice: function(value) {
425
+ var selector = document.getElementById('toolsVoiceSelector');
426
+ if (selector && Array.from(selector.options).some(opt => opt.value === value)) {
427
+ selector.value = value;
428
+ localStorage.setItem('toolsVoice', value);
429
+ }
430
+ }
310
431
  };
311
432
  })();
@@ -18,7 +18,7 @@
18
18
  var _lastVoiceBlockText = null;
19
19
  var _lastVoiceBlockTime = 0;
20
20
  var _voiceBreakNext = false;
21
- var selectedVoiceId = localStorage.getItem('voice-selected-id') || 'default';
21
+ var selectedVoiceId = localStorage.getItem('gmgui-voice-selection') || 'default';
22
22
  var ttsAudioCache = new Map();
23
23
  var TTS_CLIENT_CACHE_MAX = 50;
24
24
 
@@ -33,7 +33,7 @@
33
33
  function setupVoiceSelector() {
34
34
  var selector = document.getElementById('voiceSelector');
35
35
  if (!selector) return;
36
- var saved = localStorage.getItem('voice-selected-id');
36
+ var saved = localStorage.getItem('gmgui-voice-selection');
37
37
  if (saved) selectedVoiceId = saved;
38
38
  if (window.wsManager) {
39
39
  window.wsManager.subscribeToVoiceList(function(voices) {
@@ -66,8 +66,8 @@
66
66
  });
67
67
  selector.appendChild(grp2);
68
68
  }
69
- if (saved && selector.querySelector('option[value="' + saved + '"]')) {
70
- selector.value = saved;
69
+ if (selectedVoiceId && selector.querySelector('option[value="' + selectedVoiceId + '"]')) {
70
+ selector.value = selectedVoiceId;
71
71
  }
72
72
  });
73
73
  return;
@@ -104,15 +104,15 @@
104
104
  });
105
105
  selector.appendChild(grp2);
106
106
  }
107
- if (saved && selector.querySelector('option[value="' + saved + '"]')) {
108
- selector.value = saved;
107
+ if (selectedVoiceId && selector.querySelector('option[value="' + selectedVoiceId + '"]')) {
108
+ selector.value = selectedVoiceId;
109
109
  }
110
110
  })
111
111
  .catch(function(err) { console.error('[Voice] Failed to load voices:', err); });
112
112
  }
113
113
  selector.addEventListener('change', function() {
114
114
  selectedVoiceId = selector.value;
115
- localStorage.setItem('voice-selected-id', selectedVoiceId);
115
+ localStorage.setItem('gmgui-voice-selection', selectedVoiceId);
116
116
  sendVoiceToServer();
117
117
  });
118
118
  }
@@ -211,14 +211,14 @@
211
211
  function setupTTSToggle() {
212
212
  var toggle = document.getElementById('voiceTTSToggle');
213
213
  if (toggle) {
214
- var saved = localStorage.getItem('voice-tts-enabled');
214
+ var saved = localStorage.getItem('gmgui-auto-speak');
215
215
  if (saved !== null) {
216
216
  ttsEnabled = saved === 'true';
217
217
  toggle.checked = ttsEnabled;
218
218
  }
219
219
  toggle.addEventListener('change', function() {
220
220
  ttsEnabled = toggle.checked;
221
- localStorage.setItem('voice-tts-enabled', ttsEnabled);
221
+ localStorage.setItem('gmgui-auto-speak', ttsEnabled);
222
222
  if (!ttsEnabled) stopSpeaking();
223
223
  });
224
224
  }
@@ -352,6 +352,10 @@
352
352
 
353
353
  function speak(text) {
354
354
  if (!ttsEnabled) return;
355
+ speakDirect(text);
356
+ }
357
+
358
+ function speakDirect(text) {
355
359
  var clean = text.replace(/<[^>]*>/g, '').trim();
356
360
  if (!clean) return;
357
361
  var parts = [];
@@ -946,10 +950,39 @@
946
950
  return text.replace(/[&<>"']/g, function(c) { return map[c]; });
947
951
  }
948
952
 
953
+ function getAutoSpeak() {
954
+ return ttsEnabled;
955
+ }
956
+
957
+ function setAutoSpeak(value) {
958
+ ttsEnabled = Boolean(value);
959
+ localStorage.setItem('gmgui-auto-speak', ttsEnabled);
960
+ var toggle = document.getElementById('voiceTTSToggle');
961
+ if (toggle) toggle.checked = ttsEnabled;
962
+ if (!ttsEnabled) stopSpeaking();
963
+ }
964
+
965
+ function getVoice() {
966
+ return selectedVoiceId;
967
+ }
968
+
969
+ function setVoice(voiceId) {
970
+ selectedVoiceId = String(voiceId);
971
+ localStorage.setItem('gmgui-voice-selection', selectedVoiceId);
972
+ var selector = document.getElementById('voiceSelector');
973
+ if (selector) selector.value = selectedVoiceId;
974
+ sendVoiceToServer();
975
+ }
976
+
949
977
  window.voiceModule = {
950
978
  activate: activate,
951
979
  deactivate: deactivate,
952
- handleBlock: handleVoiceBlock
980
+ handleBlock: handleVoiceBlock,
981
+ getAutoSpeak: getAutoSpeak,
982
+ setAutoSpeak: setAutoSpeak,
983
+ getVoice: getVoice,
984
+ setVoice: setVoice,
985
+ speakText: speakDirect
953
986
  };
954
987
 
955
988
  if (document.readyState === 'loading') {
@@ -0,0 +1,551 @@
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Tools UI Redesign Test - Compact Grid Layout</title>
8
+ <style>
9
+ * { box-sizing: border-box; }
10
+
11
+ :root {
12
+ --color-primary: #3b82f6;
13
+ --color-primary-dark: #1e40af;
14
+ --color-bg-primary: #ffffff;
15
+ --color-bg-secondary: #f9fafb;
16
+ --color-bg-tertiary: #f3f4f6;
17
+ --color-text-primary: #111827;
18
+ --color-text-secondary: #6b7280;
19
+ --color-border: #e5e7eb;
20
+ --color-success: #10b981;
21
+ }
22
+
23
+ html.dark {
24
+ --color-bg-primary: #111827;
25
+ --color-bg-secondary: #1f2937;
26
+ --color-bg-tertiary: #374151;
27
+ --color-text-primary: #f9fafb;
28
+ --color-text-secondary: #d1d5db;
29
+ --color-border: #374151;
30
+ }
31
+
32
+ body {
33
+ margin: 0;
34
+ padding: 20px;
35
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
36
+ background: var(--color-bg-secondary);
37
+ color: var(--color-text-primary);
38
+ }
39
+
40
+ .test-container {
41
+ max-width: 1000px;
42
+ margin: 0 auto;
43
+ }
44
+
45
+ h1 {
46
+ font-size: 2rem;
47
+ margin-bottom: 10px;
48
+ }
49
+
50
+ .description {
51
+ color: var(--color-text-secondary);
52
+ margin-bottom: 30px;
53
+ font-size: 0.95rem;
54
+ line-height: 1.5;
55
+ }
56
+
57
+ .demo-section {
58
+ margin-bottom: 40px;
59
+ }
60
+
61
+ .section-title {
62
+ font-size: 1.25rem;
63
+ font-weight: 600;
64
+ margin-bottom: 15px;
65
+ border-bottom: 2px solid var(--color-primary);
66
+ padding-bottom: 8px;
67
+ }
68
+
69
+ /* Tools popup styles */
70
+ .tools-popup-content {
71
+ background: var(--color-bg-secondary);
72
+ border-radius: 1rem;
73
+ width: 100%;
74
+ max-width: 600px;
75
+ display: flex;
76
+ flex-direction: column;
77
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.1);
78
+ color: var(--color-text-primary);
79
+ font-family: system-ui, -apple-system, sans-serif;
80
+ overflow: hidden;
81
+ }
82
+
83
+ .tools-popup-header {
84
+ display: flex;
85
+ justify-content: space-between;
86
+ align-items: center;
87
+ padding: 0.875rem 1.25rem;
88
+ flex-shrink: 0;
89
+ border-bottom: 1px solid var(--color-border);
90
+ gap: 0.75rem;
91
+ }
92
+
93
+ .tools-popup-header h2 {
94
+ margin: 0;
95
+ font-size: 1rem;
96
+ font-weight: 700;
97
+ flex: 1;
98
+ }
99
+
100
+ .tools-popup-header-controls {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 0.75rem;
104
+ flex-shrink: 0;
105
+ }
106
+
107
+ .tools-voice-toggle {
108
+ display: flex;
109
+ align-items: center;
110
+ gap: 0.5rem;
111
+ font-size: 0.8rem;
112
+ cursor: pointer;
113
+ user-select: none;
114
+ }
115
+
116
+ .tools-voice-toggle input {
117
+ width: 0.875rem;
118
+ height: 0.875rem;
119
+ cursor: pointer;
120
+ }
121
+
122
+ .tools-voice-selector {
123
+ padding: 0.35rem 0.5rem;
124
+ font-size: 0.75rem;
125
+ border-radius: 0.3rem;
126
+ border: 1px solid var(--color-border);
127
+ background: var(--color-bg-primary);
128
+ color: var(--color-text-primary);
129
+ cursor: pointer;
130
+ }
131
+
132
+ .tools-popup-scroll {
133
+ flex: 1;
134
+ overflow-y: auto;
135
+ padding: 1rem;
136
+ display: grid;
137
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
138
+ gap: 1rem;
139
+ }
140
+
141
+ @media (max-width: 768px) {
142
+ .tools-popup-scroll {
143
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
144
+ }
145
+ }
146
+
147
+ @media (max-width: 480px) {
148
+ .tools-popup-scroll {
149
+ grid-template-columns: 1fr;
150
+ }
151
+
152
+ .tools-popup-header {
153
+ flex-direction: column;
154
+ align-items: flex-start;
155
+ }
156
+
157
+ .tools-popup-header-controls {
158
+ width: 100%;
159
+ flex-wrap: wrap;
160
+ }
161
+ }
162
+
163
+ .tool-item {
164
+ padding: 0.75rem;
165
+ border: 1px solid var(--color-border);
166
+ border-radius: 0.5rem;
167
+ background: var(--color-bg-primary);
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 0.375rem;
171
+ transition: border-color 0.2s, background-color 0.2s;
172
+ min-height: 120px;
173
+ justify-content: space-between;
174
+ }
175
+
176
+ .tool-item:hover {
177
+ border-color: var(--color-primary);
178
+ background: var(--color-bg-secondary);
179
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
180
+ }
181
+
182
+ .tool-header {
183
+ display: flex;
184
+ align-items: flex-start;
185
+ gap: 0.5rem;
186
+ justify-content: space-between;
187
+ flex-wrap: wrap;
188
+ }
189
+
190
+ .tool-name {
191
+ font-weight: 600;
192
+ font-size: 0.9rem;
193
+ flex: 1;
194
+ word-break: break-word;
195
+ }
196
+
197
+ .tool-status-indicator {
198
+ display: inline-flex;
199
+ align-items: center;
200
+ gap: 0.25rem;
201
+ font-size: 0.7rem;
202
+ padding: 0.2rem 0.4rem;
203
+ border-radius: 0.25rem;
204
+ background: var(--color-bg-tertiary);
205
+ color: var(--color-text-secondary);
206
+ white-space: nowrap;
207
+ flex-shrink: 0;
208
+ }
209
+
210
+ .tool-status-indicator.installed {
211
+ background: rgba(16, 185, 129, 0.15);
212
+ color: #10b981;
213
+ }
214
+
215
+ .tool-status-indicator.needs_update {
216
+ background: rgba(245, 158, 11, 0.15);
217
+ color: #f59e0b;
218
+ }
219
+
220
+ .tool-status-indicator.not_installed {
221
+ background: rgba(107, 114, 128, 0.15);
222
+ color: #9ca3af;
223
+ }
224
+
225
+ .tool-status-dot {
226
+ width: 0.375rem;
227
+ height: 0.375rem;
228
+ border-radius: 50%;
229
+ background: currentColor;
230
+ display: inline-block;
231
+ }
232
+
233
+ .tool-versions {
234
+ display: flex;
235
+ flex-direction: column;
236
+ gap: 0.15rem;
237
+ font-size: 0.65rem;
238
+ color: var(--color-text-secondary);
239
+ line-height: 1.3;
240
+ }
241
+
242
+ .tool-version-item {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 0.25rem;
246
+ overflow: hidden;
247
+ text-overflow: ellipsis;
248
+ }
249
+
250
+ .tool-actions {
251
+ display: flex;
252
+ gap: 0.4rem;
253
+ flex-wrap: wrap;
254
+ margin-top: 0.25rem;
255
+ }
256
+
257
+ .tool-btn {
258
+ padding: 0.35rem 0.65rem;
259
+ border: none;
260
+ border-radius: 0.3rem;
261
+ cursor: pointer;
262
+ font-size: 0.7rem;
263
+ font-weight: 600;
264
+ transition: all 0.2s;
265
+ flex: 1;
266
+ min-width: 60px;
267
+ white-space: nowrap;
268
+ }
269
+
270
+ .tool-btn-primary {
271
+ background: var(--color-primary);
272
+ color: white;
273
+ }
274
+
275
+ .tool-btn-primary:hover {
276
+ background: var(--color-primary-dark);
277
+ }
278
+
279
+ .tool-progress-bar {
280
+ width: 100%;
281
+ height: 0.25rem;
282
+ background: var(--color-bg-tertiary);
283
+ border-radius: 0.125rem;
284
+ overflow: hidden;
285
+ margin-top: 0.25rem;
286
+ }
287
+
288
+ .tool-progress-fill {
289
+ height: 100%;
290
+ background: linear-gradient(90deg, #3b82f6, #2563eb);
291
+ width: 60%;
292
+ border-radius: 0.125rem;
293
+ }
294
+
295
+ .feature-list {
296
+ display: grid;
297
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
298
+ gap: 15px;
299
+ margin: 20px 0;
300
+ }
301
+
302
+ .feature-item {
303
+ padding: 12px;
304
+ background: var(--color-bg-primary);
305
+ border-radius: 0.5rem;
306
+ border-left: 3px solid var(--color-primary);
307
+ }
308
+
309
+ .feature-item strong {
310
+ color: var(--color-primary);
311
+ }
312
+
313
+ .size-demo {
314
+ display: flex;
315
+ gap: 10px;
316
+ margin: 20px 0;
317
+ flex-wrap: wrap;
318
+ }
319
+
320
+ .size-demo-item {
321
+ text-align: center;
322
+ padding: 10px;
323
+ background: var(--color-bg-primary);
324
+ border-radius: 0.5rem;
325
+ border: 1px solid var(--color-border);
326
+ }
327
+
328
+ .size-label {
329
+ font-size: 0.8rem;
330
+ color: var(--color-text-secondary);
331
+ margin-bottom: 5px;
332
+ }
333
+
334
+ .toggle-dark {
335
+ position: fixed;
336
+ top: 20px;
337
+ right: 20px;
338
+ padding: 0.5rem 1rem;
339
+ background: var(--color-primary);
340
+ color: white;
341
+ border: none;
342
+ border-radius: 0.5rem;
343
+ cursor: pointer;
344
+ font-weight: 600;
345
+ }
346
+ </style>
347
+ </head>
348
+ <body>
349
+ <button class="toggle-dark" onclick="document.documentElement.classList.toggle('dark')">Toggle Dark</button>
350
+
351
+ <div class="test-container">
352
+ <h1>Tools UI Redesign - Compact Grid Layout</h1>
353
+ <div class="description">
354
+ This test page demonstrates the new compact single-card tools UI with integrated voice controls.
355
+ The tools are arranged in a responsive 2x2 grid that adapts to smaller screens.
356
+ </div>
357
+
358
+ <div class="demo-section">
359
+ <div class="section-title">New Features</div>
360
+ <div class="feature-list">
361
+ <div class="feature-item">
362
+ <strong>Compact Grid</strong><br>
363
+ All 4 tools fit in a 2x2 grid that's responsive
364
+ </div>
365
+ <div class="feature-item">
366
+ <strong>Integrated Voice</strong><br>
367
+ Auto-speak toggle and voice selector in header
368
+ </div>
369
+ <div class="feature-item">
370
+ <strong>Efficient Sizing</strong><br>
371
+ Tool cards ~120px height, optimized buttons
372
+ </div>
373
+ <div class="feature-item">
374
+ <strong>Responsive Design</strong><br>
375
+ Adapts from 2 columns to 1 on mobile
376
+ </div>
377
+ </div>
378
+ </div>
379
+
380
+ <div class="demo-section">
381
+ <div class="section-title">Live Preview</div>
382
+ <div style="display: flex; gap: 20px; margin-bottom: 30px; flex-wrap: wrap;">
383
+ <div>
384
+ <div class="size-label">Desktop (600px)</div>
385
+ <div class="tools-popup-content">
386
+ <div class="tools-popup-header">
387
+ <div style="display: flex; align-items: center; gap: 0.75rem; flex: 1;">
388
+ <h2 style="margin: 0; font-size: 1rem;">Tools & Extensions</h2>
389
+ </div>
390
+ <div class="tools-popup-header-controls">
391
+ <label class="tools-voice-toggle">
392
+ <input type="checkbox" checked>
393
+ <span>Auto-speak</span>
394
+ </label>
395
+ <select class="tools-voice-selector">
396
+ <option>Voice</option>
397
+ <option>en-US</option>
398
+ </select>
399
+ <button style="background: none; border: none; color: var(--color-text-secondary); font-size: 1.5rem; cursor: pointer; padding: 0; line-height: 1;">×</button>
400
+ </div>
401
+ </div>
402
+ <div class="tools-popup-scroll">
403
+ <div class="tool-item">
404
+ <div style="display: flex; flex-direction: column; gap: 0.3rem;">
405
+ <div class="tool-header">
406
+ <span class="tool-name">gm-cc</span>
407
+ </div>
408
+ <div class="tool-status-indicator installed">
409
+ <span class="tool-status-dot"></span>
410
+ <span>Up-to-date</span>
411
+ </div>
412
+ <div class="tool-versions">
413
+ <span class="tool-version-item">v1.2.5</span>
414
+ </div>
415
+ </div>
416
+ <div class="tool-actions">
417
+ <button class="tool-btn tool-btn-primary">✓</button>
418
+ </div>
419
+ </div>
420
+
421
+ <div class="tool-item">
422
+ <div style="display: flex; flex-direction: column; gap: 0.3rem;">
423
+ <div class="tool-header">
424
+ <span class="tool-name">gm-oc</span>
425
+ </div>
426
+ <div class="tool-status-indicator needs_update">
427
+ <span class="tool-status-dot"></span>
428
+ <span>Update avail</span>
429
+ </div>
430
+ <div class="tool-versions">
431
+ <span class="tool-version-item">v1.0.2</span>
432
+ <span class="tool-version-item">(v1.0.3 available)</span>
433
+ </div>
434
+ </div>
435
+ <div class="tool-actions">
436
+ <button class="tool-btn tool-btn-primary">Update</button>
437
+ </div>
438
+ </div>
439
+
440
+ <div class="tool-item">
441
+ <div style="display: flex; flex-direction: column; gap: 0.3rem;">
442
+ <div class="tool-header">
443
+ <span class="tool-name">gm-gc</span>
444
+ </div>
445
+ <div class="tool-status-indicator installed">
446
+ <span class="tool-status-dot"></span>
447
+ <span>Up-to-date</span>
448
+ </div>
449
+ <div class="tool-versions">
450
+ <span class="tool-version-item">v2.1.0</span>
451
+ </div>
452
+ </div>
453
+ <div class="tool-actions">
454
+ <button class="tool-btn tool-btn-primary">✓</button>
455
+ </div>
456
+ </div>
457
+
458
+ <div class="tool-item">
459
+ <div style="display: flex; flex-direction: column; gap: 0.3rem;">
460
+ <div class="tool-header">
461
+ <span class="tool-name">gm-kilo</span>
462
+ </div>
463
+ <div class="tool-status-indicator not_installed">
464
+ <span class="tool-status-dot"></span>
465
+ <span>Not instal</span>
466
+ </div>
467
+ <div class="tool-versions">
468
+ </div>
469
+ </div>
470
+ <div class="tool-actions">
471
+ <button class="tool-btn tool-btn-primary">Install</button>
472
+ </div>
473
+ </div>
474
+ </div>
475
+ </div>
476
+ </div>
477
+
478
+ <div>
479
+ <div class="size-label">Installing State</div>
480
+ <div class="tools-popup-content" style="max-width: 300px;">
481
+ <div class="tools-popup-header">
482
+ <h2 style="margin: 0; font-size: 1rem;">Tools & Extensions</h2>
483
+ <button style="background: none; border: none; color: var(--color-text-secondary); font-size: 1.5rem; cursor: pointer; padding: 0; line-height: 1;">×</button>
484
+ </div>
485
+ <div class="tools-popup-scroll">
486
+ <div class="tool-item">
487
+ <div style="display: flex; flex-direction: column; gap: 0.3rem;">
488
+ <div class="tool-header">
489
+ <span class="tool-name">gm-kilo</span>
490
+ </div>
491
+ <div class="tool-status-indicator" style="background: rgba(59, 130, 246, 0.15); color: #3b82f6;">
492
+ <span class="tool-status-dot"></span>
493
+ <span>Installing</span>
494
+ </div>
495
+ </div>
496
+ <div class="tool-actions">
497
+ <button class="tool-btn tool-btn-primary" disabled>Installing</button>
498
+ </div>
499
+ </div>
500
+ </div>
501
+ </div>
502
+ </div>
503
+ </div>
504
+ </div>
505
+
506
+ <div class="demo-section">
507
+ <div class="section-title">Size Comparison</div>
508
+ <div class="size-demo">
509
+ <div class="size-demo-item">
510
+ <div class="size-label">Tool Card Height</div>
511
+ <div style="font-size: 0.9rem; font-weight: 600; color: var(--color-primary);">120px min</div>
512
+ </div>
513
+ <div class="size-demo-item">
514
+ <div class="size-label">Tool Item Padding</div>
515
+ <div style="font-size: 0.9rem; font-weight: 600; color: var(--color-primary);">0.75rem</div>
516
+ </div>
517
+ <div class="size-demo-item">
518
+ <div class="size-label">Button Height</div>
519
+ <div style="font-size: 0.9rem; font-weight: 600; color: var(--color-primary);">28px</div>
520
+ </div>
521
+ <div class="size-demo-item">
522
+ <div class="size-label">Grid Gap</div>
523
+ <div style="font-size: 0.9rem; font-weight: 600; color: var(--color-primary);">1rem</div>
524
+ </div>
525
+ </div>
526
+ </div>
527
+
528
+ <div class="demo-section">
529
+ <div class="section-title">Responsive Behavior</div>
530
+ <div class="feature-list">
531
+ <div class="feature-item">
532
+ <strong>Desktop (&gt;768px)</strong><br>
533
+ 2x2 grid with 200px min-width per column
534
+ </div>
535
+ <div class="feature-item">
536
+ <strong>Tablet (481-768px)</strong><br>
537
+ Auto-fit with 150px min-width columns
538
+ </div>
539
+ <div class="feature-item">
540
+ <strong>Mobile (&lt;480px)</strong><br>
541
+ Single column layout, full width
542
+ </div>
543
+ <div class="feature-item">
544
+ <strong>Header on Mobile</strong><br>
545
+ Flex-direction column for better spacing
546
+ </div>
547
+ </div>
548
+ </div>
549
+ </div>
550
+ </body>
551
+ </html>