agentgui 1.0.507 → 1.0.508

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.
@@ -254,23 +254,74 @@ export async function checkForUpdates(toolId) {
254
254
 
255
255
  const spawnBunxProc = (pkg, onProgress) => new Promise((resolve) => {
256
256
  const cmd = isWindows ? 'bunx.cmd' : 'bunx';
257
- const proc = spawn(cmd, [pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows });
258
257
  let completed = false, stderr = '', stdout = '';
259
- const timer = setTimeout(() => { if (!completed) { completed = true; try { proc.kill('SIGKILL'); } catch (_) {} resolve({ success: false, error: 'Timeout (5min)' }); }}, 300000);
260
- proc.stdout.on('data', (d) => { stdout += d.toString(); if (onProgress) onProgress({ type: 'progress', data: d.toString() }); });
261
- proc.stderr.on('data', (d) => { stderr += d.toString(); if (onProgress) onProgress({ type: 'error', data: d.toString() }); });
258
+ let lastDataTime = Date.now();
259
+ let proc;
260
+
261
+ try {
262
+ proc = spawn(cmd, [pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows });
263
+ } catch (err) {
264
+ return resolve({ success: false, error: `Failed to spawn bunx: ${err.message}` });
265
+ }
266
+
267
+ if (!proc) {
268
+ return resolve({ success: false, error: 'Failed to spawn bunx process' });
269
+ }
270
+
271
+ const timer = setTimeout(() => {
272
+ if (!completed) {
273
+ completed = true;
274
+ try { proc.kill('SIGKILL'); } catch (_) {}
275
+ resolve({ success: false, error: 'Timeout (5min)' });
276
+ }
277
+ }, 300000);
278
+
279
+ const heartbeatTimer = setInterval(() => {
280
+ if (completed) { clearInterval(heartbeatTimer); return; }
281
+ const timeSinceLastData = Date.now() - lastDataTime;
282
+ if (timeSinceLastData > 30000) {
283
+ console.warn(`[tool-manager] No output from bunx ${pkg} for ${timeSinceLastData}ms - process may be hung`);
284
+ }
285
+ }, 30000);
286
+
287
+ const onData = (d) => {
288
+ lastDataTime = Date.now();
289
+ if (onProgress) onProgress({ type: 'progress', data: d.toString() });
290
+ };
291
+
292
+ if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d.toString(); onData(d); });
293
+ if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d.toString(); onData(d); });
294
+
262
295
  proc.on('close', (code) => {
263
296
  clearTimeout(timer);
297
+ clearInterval(heartbeatTimer);
264
298
  if (completed) return;
265
299
  completed = true;
266
300
  const output = stdout + stderr;
267
- if (code === 0 || output.includes('upgraded') || output.includes('registered') || output.includes('Hooks registered')) {
301
+ const successPatterns = [
302
+ code === 0,
303
+ output.includes('upgraded'),
304
+ output.includes('registered'),
305
+ output.includes('Hooks registered'),
306
+ output.includes('successfully'),
307
+ output.includes('Done'),
308
+ code === 0 && !output.includes('error')
309
+ ];
310
+ if (successPatterns.some(p => p)) {
268
311
  resolve({ success: true, error: null });
269
312
  } else {
270
313
  resolve({ success: false, error: output.substring(0, 1000) || 'Failed' });
271
314
  }
272
315
  });
273
- proc.on('error', (err) => { clearTimeout(timer); if (!completed) { completed = true; resolve({ success: false, error: err.message }); }});
316
+
317
+ proc.on('error', (err) => {
318
+ clearTimeout(timer);
319
+ clearInterval(heartbeatTimer);
320
+ if (!completed) {
321
+ completed = true;
322
+ resolve({ success: false, error: `Process error: ${err.message}` });
323
+ }
324
+ });
274
325
  });
275
326
 
276
327
  export async function install(toolId, onProgress) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.507",
3
+ "version": "1.0.508",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/static/index.html CHANGED
@@ -3231,6 +3231,14 @@
3231
3231
  aria-label="Message input"
3232
3232
  rows="1"
3233
3233
  ></textarea>
3234
+ <button class="voice-mic-btn" id="chatMicBtn" title="Record voice input" aria-label="Voice input">
3235
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3236
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
3237
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
3238
+ <line x1="12" y1="19" x2="12" y2="23"/>
3239
+ <line x1="8" y1="23" x2="16" y2="23"/>
3240
+ </svg>
3241
+ </button>
3234
3242
  <button class="inject-btn" id="injectBtn" title="Inject instructions into running agent" aria-label="Inject instructions">
3235
3243
  <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
3236
3244
  <path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/>
@@ -3288,6 +3296,7 @@
3288
3296
  <script defer src="/gm/js/terminal.js"></script>
3289
3297
  <script defer src="/gm/js/script-runner.js"></script>
3290
3298
  <script defer src="/gm/js/tools-manager.js"></script>
3299
+ <script defer src="/gm/js/stt-handler.js"></script>
3291
3300
  <script defer src="/gm/js/client.js"></script>
3292
3301
  <script type="module" src="/gm/js/voice.js"></script>
3293
3302
  <script defer src="/gm/js/features.js"></script>
@@ -394,6 +394,8 @@ class AgentGUIClient {
394
394
  this.ui.sendButton.addEventListener('click', () => this.startExecution());
395
395
  }
396
396
 
397
+ this.setupChatMicButton();
398
+
397
399
  this.ui.stopButton = document.getElementById('stopBtn');
398
400
  this.ui.injectButton = document.getElementById('injectBtn');
399
401
 
@@ -486,6 +488,52 @@ class AgentGUIClient {
486
488
  });
487
489
  }
488
490
 
491
+ setupChatMicButton() {
492
+ const chatMicBtn = document.getElementById('chatMicBtn');
493
+ if (!chatMicBtn) return;
494
+
495
+ let isRecording = false;
496
+
497
+ chatMicBtn.addEventListener('mousedown', async (e) => {
498
+ e.preventDefault();
499
+ if (isRecording) return;
500
+ isRecording = true;
501
+ chatMicBtn.classList.add('recording');
502
+ const result = await window.STTHandler.startRecording();
503
+ if (!result.success) {
504
+ isRecording = false;
505
+ chatMicBtn.classList.remove('recording');
506
+ alert('Microphone access denied: ' + result.error);
507
+ }
508
+ });
509
+
510
+ chatMicBtn.addEventListener('mouseup', async (e) => {
511
+ e.preventDefault();
512
+ if (!isRecording) return;
513
+ isRecording = false;
514
+ chatMicBtn.classList.remove('recording');
515
+ const result = await window.STTHandler.stopRecording();
516
+ if (result.success) {
517
+ if (this.ui.messageInput) {
518
+ this.ui.messageInput.value = result.text;
519
+ }
520
+ } else {
521
+ alert('Transcription failed: ' + result.error);
522
+ }
523
+ });
524
+
525
+ chatMicBtn.addEventListener('mouseleave', async (e) => {
526
+ if (isRecording) {
527
+ isRecording = false;
528
+ chatMicBtn.classList.remove('recording');
529
+ const result = await window.STTHandler.stopRecording();
530
+ if (result.success && this.ui.messageInput) {
531
+ this.ui.messageInput.value = result.text;
532
+ }
533
+ }
534
+ });
535
+ }
536
+
489
537
  /**
490
538
  * Connect to WebSocket
491
539
  */
@@ -0,0 +1,138 @@
1
+ (function() {
2
+ var BASE = window.__BASE_URL || '';
3
+ var isRecording = false;
4
+ var mediaStream = null;
5
+ var audioContext = null;
6
+ var workletNode = null;
7
+ var recordedChunks = [];
8
+ var TARGET_SAMPLE_RATE = 16000;
9
+
10
+ window.STTHandler = {
11
+ isRecording: function() { return isRecording; },
12
+
13
+ startRecording: async function() {
14
+ if (isRecording) return;
15
+ try {
16
+ mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
17
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
18
+ var source = audioContext.createMediaStreamSource(mediaStream);
19
+ recordedChunks = [];
20
+ await audioContext.audioWorklet.addModule(BASE + '/js/audio-recorder-processor.js');
21
+ workletNode = new AudioWorkletNode(audioContext, 'recorder-processor');
22
+ workletNode.port.onmessage = function(e) {
23
+ recordedChunks.push(e.data);
24
+ };
25
+ source.connect(workletNode);
26
+ isRecording = true;
27
+ return { success: true };
28
+ } catch (e) {
29
+ isRecording = false;
30
+ return { success: false, error: e.message };
31
+ }
32
+ },
33
+
34
+ stopRecording: async function() {
35
+ if (!isRecording) return { success: false, error: 'Not recording' };
36
+ isRecording = false;
37
+
38
+ if (workletNode) {
39
+ workletNode.port.postMessage('stop');
40
+ workletNode.disconnect();
41
+ workletNode = null;
42
+ }
43
+
44
+ if (mediaStream) {
45
+ mediaStream.getTracks().forEach(function(t) { t.stop(); });
46
+ mediaStream = null;
47
+ }
48
+
49
+ var sourceSampleRate = audioContext ? audioContext.sampleRate : 48000;
50
+ if (audioContext) {
51
+ audioContext.close().catch(function() {});
52
+ audioContext = null;
53
+ }
54
+
55
+ if (recordedChunks.length === 0) {
56
+ return { success: false, error: 'No audio recorded' };
57
+ }
58
+
59
+ var totalLen = 0;
60
+ for (var i = 0; i < recordedChunks.length; i++) totalLen += recordedChunks[i].length;
61
+ var merged = new Float32Array(totalLen);
62
+ var offset = 0;
63
+ for (var j = 0; j < recordedChunks.length; j++) {
64
+ merged.set(recordedChunks[j], offset);
65
+ offset += recordedChunks[j].length;
66
+ }
67
+ recordedChunks = [];
68
+
69
+ var resampled = resampleBuffer(merged, sourceSampleRate, TARGET_SAMPLE_RATE);
70
+ var wavBuffer = encodeWav(resampled, TARGET_SAMPLE_RATE);
71
+
72
+ try {
73
+ var resp = await fetch(BASE + '/api/stt', {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'audio/wav' },
76
+ body: wavBuffer
77
+ });
78
+ var data = await resp.json();
79
+ if (data.text) {
80
+ return { success: true, text: data.text };
81
+ } else if (data.error) {
82
+ return { success: false, error: data.error };
83
+ } else {
84
+ return { success: false, error: 'No transcription returned' };
85
+ }
86
+ } catch (e) {
87
+ return { success: false, error: 'Transcription failed: ' + e.message };
88
+ }
89
+ }
90
+ };
91
+
92
+ function resampleBuffer(inputBuffer, fromRate, toRate) {
93
+ if (fromRate === toRate) return inputBuffer;
94
+ var ratio = fromRate / toRate;
95
+ var newLen = Math.round(inputBuffer.length / ratio);
96
+ var result = new Float32Array(newLen);
97
+ for (var i = 0; i < newLen; i++) {
98
+ var srcIdx = i * ratio;
99
+ var lo = Math.floor(srcIdx);
100
+ var hi = Math.min(lo + 1, inputBuffer.length - 1);
101
+ var frac = srcIdx - lo;
102
+ result[i] = inputBuffer[lo] * (1 - frac) + inputBuffer[hi] * frac;
103
+ }
104
+ return result;
105
+ }
106
+
107
+ function encodeWav(float32Audio, sampleRate) {
108
+ var numSamples = float32Audio.length;
109
+ var bytesPerSample = 2;
110
+ var dataSize = numSamples * bytesPerSample;
111
+ var buffer = new ArrayBuffer(44 + dataSize);
112
+ var view = new DataView(buffer);
113
+
114
+ function writeStr(off, str) {
115
+ for (var i = 0; i < str.length; i++) view.setUint8(off + i, str.charCodeAt(i));
116
+ }
117
+
118
+ writeStr(0, 'RIFF');
119
+ view.setUint32(4, 36 + dataSize, true);
120
+ writeStr(8, 'WAVE');
121
+ writeStr(12, 'fmt ');
122
+ view.setUint32(16, 16, true);
123
+ view.setUint16(20, 1, true);
124
+ view.setUint16(22, 1, true);
125
+ view.setUint32(24, sampleRate, true);
126
+ view.setUint32(28, sampleRate * bytesPerSample, true);
127
+ view.setUint16(32, bytesPerSample, true);
128
+ view.setUint16(34, 16, true);
129
+ writeStr(36, 'data');
130
+ view.setUint32(40, dataSize, true);
131
+
132
+ for (var i = 0; i < numSamples; i++) {
133
+ var s = Math.max(-1, Math.min(1, float32Audio[i]));
134
+ view.setInt16(44 + i * 2, s < 0 ? s * 32768 : s * 32767, true);
135
+ }
136
+ return buffer;
137
+ }
138
+ })();
@@ -271,49 +271,6 @@
271
271
  }
272
272
  }
273
273
 
274
- function resampleBuffer(inputBuffer, fromRate, toRate) {
275
- if (fromRate === toRate) return inputBuffer;
276
- var ratio = fromRate / toRate;
277
- var newLen = Math.round(inputBuffer.length / ratio);
278
- var result = new Float32Array(newLen);
279
- for (var i = 0; i < newLen; i++) {
280
- var srcIdx = i * ratio;
281
- var lo = Math.floor(srcIdx);
282
- var hi = Math.min(lo + 1, inputBuffer.length - 1);
283
- var frac = srcIdx - lo;
284
- result[i] = inputBuffer[lo] * (1 - frac) + inputBuffer[hi] * frac;
285
- }
286
- return result;
287
- }
288
-
289
- function encodeWav(float32Audio, sampleRate) {
290
- var numSamples = float32Audio.length;
291
- var bytesPerSample = 2;
292
- var dataSize = numSamples * bytesPerSample;
293
- var buffer = new ArrayBuffer(44 + dataSize);
294
- var view = new DataView(buffer);
295
- function writeStr(off, str) {
296
- for (var i = 0; i < str.length; i++) view.setUint8(off + i, str.charCodeAt(i));
297
- }
298
- writeStr(0, 'RIFF');
299
- view.setUint32(4, 36 + dataSize, true);
300
- writeStr(8, 'WAVE');
301
- writeStr(12, 'fmt ');
302
- view.setUint32(16, 16, true);
303
- view.setUint16(20, 1, true);
304
- view.setUint16(22, 1, true);
305
- view.setUint32(24, sampleRate, true);
306
- view.setUint32(28, sampleRate * bytesPerSample, true);
307
- view.setUint16(32, bytesPerSample, true);
308
- view.setUint16(34, 16, true);
309
- writeStr(36, 'data');
310
- view.setUint32(40, dataSize, true);
311
- for (var i = 0; i < numSamples; i++) {
312
- var s = Math.max(-1, Math.min(1, float32Audio[i]));
313
- view.setInt16(44 + i * 2, s < 0 ? s * 32768 : s * 32767, true);
314
- }
315
- return buffer;
316
- }
317
274
 
318
275
  async function startRecording() {
319
276
  if (isRecording) return;
@@ -326,23 +283,13 @@
326
283
  el.setAttribute('data-final', '');
327
284
  }
328
285
  }
329
- try {
330
- mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
331
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
332
- var source = audioContext.createMediaStreamSource(mediaStream);
333
- recordedChunks = [];
334
- await audioContext.audioWorklet.addModule(BASE + '/js/audio-recorder-processor.js');
335
- workletNode = new AudioWorkletNode(audioContext, 'recorder-processor');
336
- workletNode.port.onmessage = function(e) {
337
- recordedChunks.push(e.data);
338
- };
339
- source.connect(workletNode);
286
+ var result = await window.STTHandler.startRecording();
287
+ if (result.success) {
340
288
  isRecording = true;
341
289
  var micBtn = document.getElementById('voiceMicBtn');
342
290
  if (micBtn) micBtn.classList.add('recording');
343
- } catch (e) {
344
- isRecording = false;
345
- if (el) el.textContent = 'Mic access denied or unavailable: ' + e.message;
291
+ } else {
292
+ if (el) el.textContent = 'Mic access denied: ' + result.error;
346
293
  }
347
294
  }
348
295
 
@@ -352,24 +299,7 @@
352
299
  var micBtn = document.getElementById('voiceMicBtn');
353
300
  if (micBtn) micBtn.classList.remove('recording');
354
301
  var el = document.getElementById('voiceTranscript');
355
- if (workletNode) { workletNode.port.postMessage('stop'); workletNode.disconnect(); workletNode = null; }
356
- if (mediaStream) {
357
- mediaStream.getTracks().forEach(function(t) { t.stop(); });
358
- mediaStream = null;
359
- }
360
- var sourceSampleRate = audioContext ? audioContext.sampleRate : 48000;
361
- if (audioContext) { audioContext.close().catch(function() {}); audioContext = null; }
362
- if (recordedChunks.length === 0) return;
363
- var totalLen = 0;
364
- for (var i = 0; i < recordedChunks.length; i++) totalLen += recordedChunks[i].length;
365
- var merged = new Float32Array(totalLen);
366
- var offset = 0;
367
- for (var j = 0; j < recordedChunks.length; j++) {
368
- merged.set(recordedChunks[j], offset);
369
- offset += recordedChunks[j].length;
370
- }
371
- recordedChunks = [];
372
- var resampled = resampleBuffer(merged, sourceSampleRate, TARGET_SAMPLE_RATE);
302
+
373
303
  if (el) {
374
304
  if (el.value !== undefined) {
375
305
  el.value = 'Transcribing...';
@@ -377,46 +307,23 @@
377
307
  el.textContent = 'Transcribing...';
378
308
  }
379
309
  }
380
- try {
381
- var wavBuffer = encodeWav(resampled, TARGET_SAMPLE_RATE);
382
- var resp = await fetch(BASE + '/api/stt', {
383
- method: 'POST',
384
- headers: { 'Content-Type': 'audio/wav' },
385
- body: wavBuffer
386
- });
387
- var data = await resp.json();
388
- if (data.text) {
389
- if (el) {
390
- if (el.value !== undefined) {
391
- el.value = data.text;
392
- } else {
393
- el.textContent = data.text;
394
- el.setAttribute('data-final', data.text);
395
- }
396
- }
397
- } else if (data.error) {
398
- if (el) {
399
- if (el.value !== undefined) {
400
- el.value = 'Error: ' + data.error;
401
- } else {
402
- el.textContent = 'Error: ' + data.error;
403
- }
404
- }
405
- } else {
406
- if (el) {
407
- if (el.value !== undefined) {
408
- el.value = '';
409
- } else {
410
- el.textContent = '';
411
- }
310
+
311
+ var result = await window.STTHandler.stopRecording();
312
+ if (result.success) {
313
+ if (el) {
314
+ if (el.value !== undefined) {
315
+ el.value = result.text;
316
+ } else {
317
+ el.textContent = result.text;
318
+ el.setAttribute('data-final', result.text);
412
319
  }
413
320
  }
414
- } catch (e) {
321
+ } else {
415
322
  if (el) {
416
323
  if (el.value !== undefined) {
417
- el.value = 'Transcription failed: ' + e.message;
324
+ el.value = 'Error: ' + result.error;
418
325
  } else {
419
- el.textContent = 'Transcription failed: ' + e.message;
326
+ el.textContent = 'Error: ' + result.error;
420
327
  }
421
328
  }
422
329
  }