agentgui 1.0.512 → 1.0.514
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/tool-manager.js +17 -15
- package/package.json +1 -1
- package/server.js +10 -30
- package/static/js/voice.js +38 -0
- package/test-fixes.mjs +103 -0
package/lib/tool-manager.js
CHANGED
|
@@ -6,10 +6,10 @@ import path from 'path';
|
|
|
6
6
|
|
|
7
7
|
const isWindows = os.platform() === 'win32';
|
|
8
8
|
const TOOLS = [
|
|
9
|
-
{ id: 'gm-oc', name: 'OpenCode', pkg: 'opencode-ai' },
|
|
10
|
-
{ id: 'gm-gc', name: 'Gemini CLI', pkg: '@google/gemini-cli' },
|
|
11
|
-
{ id: 'gm-kilo', name: 'Kilo', pkg: '@kilocode/cli' },
|
|
12
|
-
{ id: 'gm-cc', name: 'Claude Code', pkg: '@anthropic-ai/claude-code' },
|
|
9
|
+
{ id: 'gm-oc', name: 'OpenCode', pkg: 'opencode-ai', pluginId: 'opencode-ai' },
|
|
10
|
+
{ id: 'gm-gc', name: 'Gemini CLI', pkg: '@google/gemini-cli', pluginId: 'gm' },
|
|
11
|
+
{ id: 'gm-kilo', name: 'Kilo', pkg: '@kilocode/cli', pluginId: '@kilocode/cli' },
|
|
12
|
+
{ id: 'gm-cc', name: 'Claude Code', pkg: '@anthropic-ai/claude-code', pluginId: 'gm' },
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
const statusCache = new Map();
|
|
@@ -23,12 +23,14 @@ const getNodeModulesPath = () => {
|
|
|
23
23
|
return path.join(__dirname, '..', 'node_modules');
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
const getInstalledVersion = (pkg) => {
|
|
26
|
+
const getInstalledVersion = (pkg, pluginId = null) => {
|
|
27
27
|
try {
|
|
28
28
|
const homeDir = os.homedir();
|
|
29
|
+
const tool = pluginId ? TOOLS.find(t => t.pkg === pkg) : null;
|
|
30
|
+
const actualPluginId = pluginId || (tool?.pluginId) || pkg;
|
|
29
31
|
|
|
30
|
-
// Check Claude Code plugins
|
|
31
|
-
const claudePath = path.join(homeDir, '.claude', 'plugins',
|
|
32
|
+
// Check Claude Code plugins using correct pluginId
|
|
33
|
+
const claudePath = path.join(homeDir, '.claude', 'plugins', actualPluginId, 'plugin.json');
|
|
32
34
|
if (fs.existsSync(claudePath)) {
|
|
33
35
|
try {
|
|
34
36
|
const pluginJson = JSON.parse(fs.readFileSync(claudePath, 'utf-8'));
|
|
@@ -38,8 +40,8 @@ const getInstalledVersion = (pkg) => {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
// Check OpenCode agents
|
|
42
|
-
const opencodePath = path.join(homeDir, '.config', 'opencode', 'agents',
|
|
43
|
+
// Check OpenCode agents using correct pluginId
|
|
44
|
+
const opencodePath = path.join(homeDir, '.config', 'opencode', 'agents', actualPluginId, 'plugin.json');
|
|
43
45
|
if (fs.existsSync(opencodePath)) {
|
|
44
46
|
try {
|
|
45
47
|
const pluginJson = JSON.parse(fs.readFileSync(opencodePath, 'utf-8'));
|
|
@@ -50,7 +52,7 @@ const getInstalledVersion = (pkg) => {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Check Gemini CLI agents (stored as 'gm' directory)
|
|
53
|
-
const geminiPath = path.join(homeDir, '.gemini', 'extensions',
|
|
55
|
+
const geminiPath = path.join(homeDir, '.gemini', 'extensions', actualPluginId, 'plugin.json');
|
|
54
56
|
if (fs.existsSync(geminiPath)) {
|
|
55
57
|
try {
|
|
56
58
|
const pluginJson = JSON.parse(fs.readFileSync(geminiPath, 'utf-8'));
|
|
@@ -60,7 +62,7 @@ const getInstalledVersion = (pkg) => {
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
// Try gemini-extension.json as fallback
|
|
63
|
-
const geminiExtPath = path.join(homeDir, '.gemini', 'extensions',
|
|
65
|
+
const geminiExtPath = path.join(homeDir, '.gemini', 'extensions', actualPluginId, 'gemini-extension.json');
|
|
64
66
|
if (fs.existsSync(geminiExtPath)) {
|
|
65
67
|
try {
|
|
66
68
|
const extJson = JSON.parse(fs.readFileSync(geminiExtPath, 'utf-8'));
|
|
@@ -70,8 +72,8 @@ const getInstalledVersion = (pkg) => {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
// Check Kilo agents
|
|
74
|
-
const kiloPath = path.join(homeDir, '.config', 'kilo', 'agents',
|
|
75
|
+
// Check Kilo agents using correct pluginId
|
|
76
|
+
const kiloPath = path.join(homeDir, '.config', 'kilo', 'agents', actualPluginId, 'plugin.json');
|
|
75
77
|
if (fs.existsSync(kiloPath)) {
|
|
76
78
|
try {
|
|
77
79
|
const pluginJson = JSON.parse(fs.readFileSync(kiloPath, 'utf-8'));
|
|
@@ -379,7 +381,7 @@ export async function install(toolId, onProgress) {
|
|
|
379
381
|
statusCache.delete(toolId);
|
|
380
382
|
versionCache.clear();
|
|
381
383
|
|
|
382
|
-
const version = getInstalledVersion(tool.pkg);
|
|
384
|
+
const version = getInstalledVersion(tool.pkg, tool.pluginId);
|
|
383
385
|
if (!version) {
|
|
384
386
|
console.warn(`[tool-manager] Install succeeded but version detection failed for ${toolId}. Attempting CLI check...`);
|
|
385
387
|
const cliVersion = await getCliToolVersion(tool.pkg);
|
|
@@ -413,7 +415,7 @@ export async function update(toolId, onProgress) {
|
|
|
413
415
|
statusCache.delete(toolId);
|
|
414
416
|
versionCache.clear();
|
|
415
417
|
|
|
416
|
-
const version = getInstalledVersion(tool.pkg);
|
|
418
|
+
const version = getInstalledVersion(tool.pkg, tool.pluginId);
|
|
417
419
|
if (!version) {
|
|
418
420
|
console.warn(`[tool-manager] Update succeeded but version detection failed for ${toolId}. Attempting CLI check...`);
|
|
419
421
|
const cliVersion = await getCliToolVersion(tool.pkg);
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1863,17 +1863,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1863
1863
|
if (!installCompleted) {
|
|
1864
1864
|
installCompleted = true;
|
|
1865
1865
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: 'Install timeout after 6 minutes' });
|
|
1866
|
-
|
|
1867
|
-
wsOptimizer.broadcast({ type: 'tool_install_failed', toolId, data: { success: false, error: 'Install timeout after 6 minutes' } });
|
|
1868
|
-
}
|
|
1866
|
+
broadcastSync({ type: 'tool_install_failed', toolId, data: { success: false, error: 'Install timeout after 6 minutes' } });
|
|
1869
1867
|
queries.addToolInstallHistory(toolId, 'install', 'failed', 'Install timeout after 6 minutes');
|
|
1870
1868
|
}
|
|
1871
1869
|
}, 360000);
|
|
1872
1870
|
|
|
1873
1871
|
toolManager.install(toolId, (msg) => {
|
|
1874
|
-
|
|
1875
|
-
wsOptimizer.broadcast({ type: 'tool_install_progress', toolId, data: msg });
|
|
1876
|
-
}
|
|
1872
|
+
broadcastSync({ type: 'tool_install_progress', toolId, data: msg });
|
|
1877
1873
|
}).then(async (result) => {
|
|
1878
1874
|
clearTimeout(installTimeout);
|
|
1879
1875
|
if (installCompleted) return;
|
|
@@ -1884,16 +1880,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1884
1880
|
queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
|
|
1885
1881
|
const freshStatus = await toolManager.checkToolStatusAsync(toolId);
|
|
1886
1882
|
console.log(`[TOOLS-API] Fresh status after install for ${toolId}:`, JSON.stringify(freshStatus));
|
|
1887
|
-
|
|
1888
|
-
wsOptimizer.broadcast({ type: 'tool_install_complete', toolId, data: { success: true, ...freshStatus } });
|
|
1889
|
-
}
|
|
1883
|
+
broadcastSync({ type: 'tool_install_complete', toolId, data: { success: true, ...freshStatus } });
|
|
1890
1884
|
queries.addToolInstallHistory(toolId, 'install', 'success', null);
|
|
1891
1885
|
} else {
|
|
1892
1886
|
console.error(`[TOOLS-API] Install failed for ${toolId}:`, result.error);
|
|
1893
1887
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
|
|
1894
|
-
|
|
1895
|
-
wsOptimizer.broadcast({ type: 'tool_install_failed', toolId, data: result });
|
|
1896
|
-
}
|
|
1888
|
+
broadcastSync({ type: 'tool_install_failed', toolId, data: result });
|
|
1897
1889
|
queries.addToolInstallHistory(toolId, 'install', 'failed', result.error);
|
|
1898
1890
|
}
|
|
1899
1891
|
}).catch((err) => {
|
|
@@ -1903,9 +1895,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1903
1895
|
const error = err?.message || 'Unknown error';
|
|
1904
1896
|
console.error(`[TOOLS-API] Install error for ${toolId}:`, error);
|
|
1905
1897
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
|
|
1906
|
-
|
|
1907
|
-
wsOptimizer.broadcast({ type: 'tool_install_failed', toolId, data: { success: false, error } });
|
|
1908
|
-
}
|
|
1898
|
+
broadcastSync({ type: 'tool_install_failed', toolId, data: { success: false, error } });
|
|
1909
1899
|
queries.addToolInstallHistory(toolId, 'install', 'failed', error);
|
|
1910
1900
|
});
|
|
1911
1901
|
return;
|
|
@@ -1932,17 +1922,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1932
1922
|
if (!updateCompleted) {
|
|
1933
1923
|
updateCompleted = true;
|
|
1934
1924
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: 'Update timeout after 6 minutes' });
|
|
1935
|
-
|
|
1936
|
-
wsOptimizer.broadcast({ type: 'tool_update_failed', toolId, data: { success: false, error: 'Update timeout after 6 minutes' } });
|
|
1937
|
-
}
|
|
1925
|
+
broadcastSync({ type: 'tool_update_failed', toolId, data: { success: false, error: 'Update timeout after 6 minutes' } });
|
|
1938
1926
|
queries.addToolInstallHistory(toolId, 'update', 'failed', 'Update timeout after 6 minutes');
|
|
1939
1927
|
}
|
|
1940
1928
|
}, 360000);
|
|
1941
1929
|
|
|
1942
1930
|
toolManager.update(toolId, (msg) => {
|
|
1943
|
-
|
|
1944
|
-
wsOptimizer.broadcast({ type: 'tool_update_progress', toolId, data: msg });
|
|
1945
|
-
}
|
|
1931
|
+
broadcastSync({ type: 'tool_update_progress', toolId, data: msg });
|
|
1946
1932
|
}).then(async (result) => {
|
|
1947
1933
|
clearTimeout(updateTimeout);
|
|
1948
1934
|
if (updateCompleted) return;
|
|
@@ -1953,16 +1939,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1953
1939
|
queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
|
|
1954
1940
|
const freshStatus = await toolManager.checkToolStatusAsync(toolId);
|
|
1955
1941
|
console.log(`[TOOLS-API] Fresh status after update for ${toolId}:`, JSON.stringify(freshStatus));
|
|
1956
|
-
|
|
1957
|
-
wsOptimizer.broadcast({ type: 'tool_update_complete', toolId, data: { success: true, ...freshStatus } });
|
|
1958
|
-
}
|
|
1942
|
+
broadcastSync({ type: 'tool_update_complete', toolId, data: { success: true, ...freshStatus } });
|
|
1959
1943
|
queries.addToolInstallHistory(toolId, 'update', 'success', null);
|
|
1960
1944
|
} else {
|
|
1961
1945
|
console.error(`[TOOLS-API] Update failed for ${toolId}:`, result.error);
|
|
1962
1946
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
|
|
1963
|
-
|
|
1964
|
-
wsOptimizer.broadcast({ type: 'tool_update_failed', toolId, data: result });
|
|
1965
|
-
}
|
|
1947
|
+
broadcastSync({ type: 'tool_update_failed', toolId, data: result });
|
|
1966
1948
|
queries.addToolInstallHistory(toolId, 'update', 'failed', result.error);
|
|
1967
1949
|
}
|
|
1968
1950
|
}).catch((err) => {
|
|
@@ -1972,9 +1954,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1972
1954
|
const error = err?.message || 'Unknown error';
|
|
1973
1955
|
console.error(`[TOOLS-API] Update error for ${toolId}:`, error);
|
|
1974
1956
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
|
|
1975
|
-
|
|
1976
|
-
wsOptimizer.broadcast({ type: 'tool_update_failed', toolId, data: { success: false, error } });
|
|
1977
|
-
}
|
|
1957
|
+
broadcastSync({ type: 'tool_update_failed', toolId, data: { success: false, error } });
|
|
1978
1958
|
queries.addToolInstallHistory(toolId, 'update', 'failed', error);
|
|
1979
1959
|
});
|
|
1980
1960
|
return;
|
package/static/js/voice.js
CHANGED
|
@@ -453,6 +453,43 @@
|
|
|
453
453
|
});
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
function preGenerateTTS(text) {
|
|
457
|
+
if (!ttsEnabled) return;
|
|
458
|
+
var clean = text.replace(/<[^>]*>/g, '').trim();
|
|
459
|
+
if (!clean) return;
|
|
460
|
+
var parts = [];
|
|
461
|
+
if (typeof agentGUIClient !== 'undefined' && agentGUIClient && typeof agentGUIClient.parseMarkdownCodeBlocks === 'function') {
|
|
462
|
+
parts = agentGUIClient.parseMarkdownCodeBlocks(clean);
|
|
463
|
+
} else {
|
|
464
|
+
parts = [{ type: 'text', content: clean }];
|
|
465
|
+
}
|
|
466
|
+
parts.forEach(function(part) {
|
|
467
|
+
if (part.type === 'code') return;
|
|
468
|
+
var segment = part.content.trim();
|
|
469
|
+
if (!segment) return;
|
|
470
|
+
var cacheKey = selectedVoiceId + ':' + segment;
|
|
471
|
+
if (ttsAudioCache.has(cacheKey)) return;
|
|
472
|
+
var optimizedText = optimizePromptForSpeech(segment);
|
|
473
|
+
fetch(BASE + '/api/tts', {
|
|
474
|
+
method: 'POST',
|
|
475
|
+
headers: { 'Content-Type': 'application/json' },
|
|
476
|
+
body: JSON.stringify({ text: optimizedText, voiceId: selectedVoiceId })
|
|
477
|
+
}).then(function(resp) {
|
|
478
|
+
if (!resp.ok) throw new Error('TTS pre-generation failed: ' + resp.status);
|
|
479
|
+
return resp.arrayBuffer();
|
|
480
|
+
}).then(function(buf) {
|
|
481
|
+
var blob = new Blob([buf], { type: 'audio/wav' });
|
|
482
|
+
if (ttsAudioCache.size >= TTS_CLIENT_CACHE_MAX) {
|
|
483
|
+
var oldest = ttsAudioCache.keys().next().value;
|
|
484
|
+
ttsAudioCache.delete(oldest);
|
|
485
|
+
}
|
|
486
|
+
ttsAudioCache.set(cacheKey, blob);
|
|
487
|
+
}).catch(function(err) {
|
|
488
|
+
console.warn('[Voice] TTS pre-generation failed:', err);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
456
493
|
function processQueue() {
|
|
457
494
|
if (isSpeaking || speechQueue.length === 0) return;
|
|
458
495
|
if (ttsDisabledUntilReset) {
|
|
@@ -783,6 +820,7 @@
|
|
|
783
820
|
var div = addVoiceBlock(block.text, isUser);
|
|
784
821
|
if (div && isNew && ttsEnabled && blockRole === 'assistant') {
|
|
785
822
|
div.classList.add('speaking');
|
|
823
|
+
preGenerateTTS(block.text);
|
|
786
824
|
speak(block.text);
|
|
787
825
|
setTimeout(function() { div.classList.remove('speaking'); }, 2000);
|
|
788
826
|
}
|
package/test-fixes.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test script for validating the two fixes:
|
|
5
|
+
* 1. Agent selector visibility in chat view
|
|
6
|
+
* 2. TTS streaming pre-generation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
console.log('=== AgentGUI Fix Validation ===\n');
|
|
13
|
+
|
|
14
|
+
// Test 1: Verify agent selectors are NOT hidden on desktop
|
|
15
|
+
console.log('Test 1: Agent Selector Visibility');
|
|
16
|
+
console.log('-----------------------------------');
|
|
17
|
+
const html = fs.readFileSync('./static/index.html', 'utf-8');
|
|
18
|
+
const beforeMobileMedia = html.substring(0, html.indexOf('@media (max-width: 480px)'));
|
|
19
|
+
const cliHiddenOnDesktop = beforeMobileMedia.includes('.cli-selector { display: none; }');
|
|
20
|
+
const modelHiddenOnDesktop = beforeMobileMedia.includes('.model-selector { display: none; }');
|
|
21
|
+
|
|
22
|
+
if (!cliHiddenOnDesktop && !modelHiddenOnDesktop) {
|
|
23
|
+
console.log('✓ PASS: Agent selectors are NOT hidden by CSS on desktop');
|
|
24
|
+
console.log(' - CLI selector will be visible when populated');
|
|
25
|
+
console.log(' - Model selector will be visible when populated');
|
|
26
|
+
} else {
|
|
27
|
+
console.log('❌ FAIL: Agent selectors are still hidden on desktop');
|
|
28
|
+
if (cliHiddenOnDesktop) console.log(' - .cli-selector has display:none');
|
|
29
|
+
if (modelHiddenOnDesktop) console.log(' - .model-selector has display:none');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Verify selectors are in HTML
|
|
33
|
+
const hasSelectors = html.includes('data-cli-selector') &&
|
|
34
|
+
html.includes('data-agent-selector') &&
|
|
35
|
+
html.includes('data-model-selector');
|
|
36
|
+
if (hasSelectors) {
|
|
37
|
+
console.log('✓ PASS: All selector elements are present in HTML');
|
|
38
|
+
} else {
|
|
39
|
+
console.log('❌ FAIL: Some selector elements are missing');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Test 2: Verify TTS pre-generation is implemented
|
|
43
|
+
console.log('\nTest 2: TTS Streaming Pre-generation');
|
|
44
|
+
console.log('--------------------------------------');
|
|
45
|
+
const voiceJs = fs.readFileSync('./static/js/voice.js', 'utf-8');
|
|
46
|
+
const hasPreGenerateFunction = voiceJs.includes('function preGenerateTTS(text)');
|
|
47
|
+
const callsPreGenerate = voiceJs.includes('preGenerateTTS(block.text)');
|
|
48
|
+
const usesCache = voiceJs.includes('if (ttsAudioCache.has(cacheKey)) return;');
|
|
49
|
+
const fetchesTTS = voiceJs.match(/fetch\(BASE \+ '\/api\/tts'/g)?.length >= 2; // Should be in both preGenerate and processQueue
|
|
50
|
+
|
|
51
|
+
if (hasPreGenerateFunction && callsPreGenerate && usesCache && fetchesTTS) {
|
|
52
|
+
console.log('✓ PASS: TTS pre-generation is fully implemented');
|
|
53
|
+
console.log(' - preGenerateTTS function exists');
|
|
54
|
+
console.log(' - Called when assistant text arrives');
|
|
55
|
+
console.log(' - Uses cache to avoid duplicate generation');
|
|
56
|
+
console.log(' - Audio generation happens in background');
|
|
57
|
+
} else {
|
|
58
|
+
console.log('❌ FAIL: TTS pre-generation incomplete');
|
|
59
|
+
if (!hasPreGenerateFunction) console.log(' - Missing preGenerateTTS function');
|
|
60
|
+
if (!callsPreGenerate) console.log(' - Not called in handleVoiceBlock');
|
|
61
|
+
if (!usesCache) console.log(' - Missing cache check');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Test 3: Check that client.js populates selectors correctly
|
|
65
|
+
console.log('\nTest 3: Selector Population Logic');
|
|
66
|
+
console.log('-----------------------------------');
|
|
67
|
+
const clientJs = fs.readFileSync('./static/js/client.js', 'utf-8');
|
|
68
|
+
const setsDisplay = clientJs.includes("this.ui.cliSelector.style.display = 'inline-block'");
|
|
69
|
+
const populatesOptions = clientJs.includes('.innerHTML = displayAgents');
|
|
70
|
+
|
|
71
|
+
if (setsDisplay && populatesOptions) {
|
|
72
|
+
console.log('✓ PASS: Client.js correctly populates and displays selectors');
|
|
73
|
+
console.log(' - Sets display to inline-block when agents loaded');
|
|
74
|
+
console.log(' - Populates options from agent list');
|
|
75
|
+
} else {
|
|
76
|
+
console.log('❌ FAIL: Selector population logic may be broken');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Summary
|
|
80
|
+
console.log('\n=== SUMMARY ===');
|
|
81
|
+
const allTestsPass = !cliHiddenOnDesktop && !modelHiddenOnDesktop &&
|
|
82
|
+
hasSelectors && hasPreGenerateFunction &&
|
|
83
|
+
callsPreGenerate && usesCache &&
|
|
84
|
+
setsDisplay && populatesOptions;
|
|
85
|
+
|
|
86
|
+
if (allTestsPass) {
|
|
87
|
+
console.log('✓ ALL TESTS PASSED\n');
|
|
88
|
+
console.log('Manual Testing Instructions:');
|
|
89
|
+
console.log('1. Start server: npm run dev');
|
|
90
|
+
console.log('2. Open http://localhost:3000/gm/');
|
|
91
|
+
console.log('3. Check chat input area - you should see:');
|
|
92
|
+
console.log(' - CLI selector dropdown (e.g., "Claude")');
|
|
93
|
+
console.log(' - Model selector dropdown (e.g., "Sonnet 4.5")');
|
|
94
|
+
console.log(' - Microphone button for voice input');
|
|
95
|
+
console.log('4. Open Voice tab and enable "Auto-speak responses"');
|
|
96
|
+
console.log('5. Send a message and observe:');
|
|
97
|
+
console.log(' - Text streams in as agent responds');
|
|
98
|
+
console.log(' - Voice playback starts immediately after completion (no delay)');
|
|
99
|
+
console.log(' - Audio was pre-generated during streaming');
|
|
100
|
+
} else {
|
|
101
|
+
console.log('❌ SOME TESTS FAILED - Review output above\n');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|