mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
chore: bump version to 0.1645
This commit is contained in:
@@ -35,7 +35,7 @@ When user says "COM":
|
||||
1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`)
|
||||
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
|
||||
|
||||
**Version**: 0.1644 (must match `package.json`)
|
||||
**Version**: 0.1645 (must match `package.json`)
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1644",
|
||||
"version": "0.1645",
|
||||
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
+100
-18
@@ -603,6 +603,7 @@ const DeepgramProvider = {
|
||||
_mediaRecorder: null,
|
||||
_stream: null,
|
||||
_silenceTimeout: null,
|
||||
_keepAliveInterval: null,
|
||||
_onResult: null,
|
||||
_onError: null,
|
||||
_onEnd: null,
|
||||
@@ -632,11 +633,22 @@ const DeepgramProvider = {
|
||||
// Notify caller so it can set up audio level meter
|
||||
opts.onStream?.(this._stream);
|
||||
|
||||
// 2. Build WebSocket URL
|
||||
// 2. Detect best supported MIME type for MediaRecorder
|
||||
const mimeTypes = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
|
||||
this._selectedMime = null;
|
||||
for (const mt of mimeTypes) {
|
||||
if (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported(mt)) {
|
||||
this._selectedMime = mt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Build WebSocket URL (no encoding param — Deepgram auto-detects from container format)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
model: 'nova-3',
|
||||
smart_format: 'true',
|
||||
punctuate: 'true',
|
||||
smart_format: 'false',
|
||||
punctuate: 'false',
|
||||
interim_results: 'true',
|
||||
utterance_end_ms: '1500',
|
||||
vad_events: 'true',
|
||||
@@ -649,14 +661,20 @@ const DeepgramProvider = {
|
||||
if (opts.keyterms?.length) {
|
||||
for (const term of opts.keyterms) {
|
||||
const trimmed = term.trim();
|
||||
if (trimmed) params.append('keywords', trimmed + ':2');
|
||||
if (trimmed) params.append('keyterm', trimmed + ':2');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Connect WebSocket
|
||||
// 4. Connect WebSocket (trim API key to avoid whitespace auth failures)
|
||||
const apiKey = (opts.apiKey || '').trim();
|
||||
if (!apiKey) {
|
||||
this._onError?.('No Deepgram API key configured. Add one in Settings > Voice.');
|
||||
this._cleanup();
|
||||
return;
|
||||
}
|
||||
const wsUrl = `wss://api.deepgram.com/v1/listen?${params}`;
|
||||
try {
|
||||
this._ws = new WebSocket(wsUrl, ['token', opts.apiKey]);
|
||||
this._ws = new WebSocket(wsUrl, ['token', apiKey]);
|
||||
} catch (err) {
|
||||
this._onError?.('Failed to connect to Deepgram: ' + err.message);
|
||||
this._cleanup();
|
||||
@@ -664,7 +682,14 @@ const DeepgramProvider = {
|
||||
}
|
||||
|
||||
this._ws.onopen = () => {
|
||||
// 4. Start MediaRecorder once connected
|
||||
// 5. Send KeepAlive every 8s to prevent Deepgram from closing idle connections
|
||||
// (covers the gap before MediaRecorder produces its first chunk)
|
||||
this._keepAliveInterval = setInterval(() => {
|
||||
if (this._ws?.readyState === WebSocket.OPEN) {
|
||||
try { this._ws.send(JSON.stringify({ type: 'KeepAlive' })); } catch (_e) { /* ignore */ }
|
||||
}
|
||||
}, 8000);
|
||||
// 6. Start MediaRecorder once connected
|
||||
this._startRecording();
|
||||
};
|
||||
|
||||
@@ -690,8 +715,13 @@ const DeepgramProvider = {
|
||||
};
|
||||
|
||||
this._ws.onclose = (event) => {
|
||||
clearInterval(this._keepAliveInterval);
|
||||
this._keepAliveInterval = null;
|
||||
if (event.code === 1008) {
|
||||
this._onError?.('Authentication failed. Check your Deepgram API key in Settings > Voice.');
|
||||
} else if (event.code === 1006) {
|
||||
// 1006 = abnormal closure (no close frame). Usually auth failure, expired key, or no credits.
|
||||
this._onError?.('Deepgram connection failed (1006). Check your API key is valid and has credits in Settings > Voice.');
|
||||
} else if (event.code !== 1000) {
|
||||
this._onError?.('Deepgram connection closed: ' + (event.reason || `code ${event.code}`));
|
||||
}
|
||||
@@ -703,17 +733,7 @@ const DeepgramProvider = {
|
||||
_startRecording() {
|
||||
if (!this._stream || !this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
// Pick best supported MIME type
|
||||
const mimeTypes = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
|
||||
let mimeType;
|
||||
for (const mt of mimeTypes) {
|
||||
if (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported(mt)) {
|
||||
mimeType = mt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const recorderOpts = mimeType ? { mimeType } : {};
|
||||
const recorderOpts = this._selectedMime ? { mimeType: this._selectedMime } : {};
|
||||
try {
|
||||
this._mediaRecorder = new MediaRecorder(this._stream, recorderOpts);
|
||||
} catch (err) {
|
||||
@@ -752,6 +772,8 @@ const DeepgramProvider = {
|
||||
stop() {
|
||||
clearTimeout(this._silenceTimeout);
|
||||
this._silenceTimeout = null;
|
||||
clearInterval(this._keepAliveInterval);
|
||||
this._keepAliveInterval = null;
|
||||
this._stopRecording();
|
||||
if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
||||
try { this._ws.close(1000); } catch (_e) { /* ignore */ }
|
||||
@@ -763,6 +785,7 @@ const DeepgramProvider = {
|
||||
this._ws = null;
|
||||
this._mediaRecorder = null;
|
||||
this._stream = null;
|
||||
this._selectedMime = null;
|
||||
this._onResult = null;
|
||||
this._onError = null;
|
||||
this._onEnd = null;
|
||||
@@ -1092,10 +1115,68 @@ const VoiceInput = {
|
||||
} else {
|
||||
app.sendInput(trimmed).catch(() => {});
|
||||
}
|
||||
this._showVoiceSendBtn();
|
||||
setTimeout(() => { if (app.terminal) app.terminal.focus(); }, 150);
|
||||
}
|
||||
},
|
||||
|
||||
/** Show a green Enter button by transforming the gear icon in-place */
|
||||
_showVoiceSendBtn() {
|
||||
// Find the gear button (mobile or desktop header)
|
||||
const gear = document.querySelector('.btn-settings-mobile') || document.querySelector('.btn-settings');
|
||||
if (!gear || gear.classList.contains('voice-send-active')) return;
|
||||
|
||||
// Remove existing if any
|
||||
this._hideVoiceSendBtn();
|
||||
|
||||
// Save original state
|
||||
this._voiceSendGear = gear;
|
||||
this._voiceSendOriginalHTML = gear.innerHTML;
|
||||
this._voiceSendOriginalOnclick = gear.getAttribute('onclick');
|
||||
|
||||
// Transform into green send button
|
||||
gear.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>';
|
||||
gear.classList.add('voice-send-active');
|
||||
gear.removeAttribute('onclick');
|
||||
gear.title = 'Send (Enter)';
|
||||
|
||||
// Click handler
|
||||
this._voiceSendHandler = () => {
|
||||
if (!app.activeSessionId) return;
|
||||
// Simulate Enter key: if local echo is active, flush its buffer + send \r;
|
||||
// otherwise just send \r directly to the PTY
|
||||
if (app._localEchoEnabled && app._localEchoOverlay) {
|
||||
const text = app._localEchoOverlay.pendingText || '';
|
||||
app._localEchoOverlay.clear();
|
||||
app._localEchoOverlay.suppressBufferDetection();
|
||||
if (text) app.sendInput(text).catch(() => {});
|
||||
setTimeout(() => app.sendInput('\r').catch(() => {}), 80);
|
||||
} else {
|
||||
app.sendInput('\r').catch(() => {});
|
||||
}
|
||||
// Blink then restore
|
||||
gear.classList.add('voice-send-blink');
|
||||
setTimeout(() => this._hideVoiceSendBtn(), 400);
|
||||
};
|
||||
gear.addEventListener('click', this._voiceSendHandler);
|
||||
},
|
||||
|
||||
_hideVoiceSendBtn() {
|
||||
const gear = this._voiceSendGear;
|
||||
if (!gear) return;
|
||||
gear.removeEventListener('click', this._voiceSendHandler);
|
||||
gear.classList.remove('voice-send-active', 'voice-send-blink');
|
||||
gear.innerHTML = this._voiceSendOriginalHTML || '';
|
||||
if (this._voiceSendOriginalOnclick) {
|
||||
gear.setAttribute('onclick', this._voiceSendOriginalOnclick);
|
||||
}
|
||||
gear.title = 'App Settings';
|
||||
this._voiceSendGear = null;
|
||||
this._voiceSendHandler = null;
|
||||
this._voiceSendOriginalHTML = null;
|
||||
this._voiceSendOriginalOnclick = null;
|
||||
},
|
||||
|
||||
/** Show an editable compose overlay so the user can review/edit before sending */
|
||||
_showComposeOverlay(text) {
|
||||
document.querySelector('.voice-compose-overlay')?.remove();
|
||||
@@ -1315,6 +1396,7 @@ const VoiceInput = {
|
||||
/** Cleanup on SSE reconnect or page unload */
|
||||
cleanup() {
|
||||
if (this.isRecording) this.stop();
|
||||
this._hideVoiceSendBtn();
|
||||
DeepgramProvider._cleanup();
|
||||
this.recognition = null;
|
||||
this._activeProvider = null;
|
||||
|
||||
@@ -2142,6 +2142,38 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Voice send button — gear icon transforms into green Enter after voice input */
|
||||
.voice-send-active {
|
||||
background: rgba(34, 197, 94, 0.2) !important;
|
||||
border-color: rgba(34, 197, 94, 0.7) !important;
|
||||
color: #22c55e !important;
|
||||
animation: voice-send-glow 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.voice-send-active:active {
|
||||
transform: scale(0.9);
|
||||
background: rgba(34, 197, 94, 0.4) !important;
|
||||
}
|
||||
|
||||
@keyframes voice-send-glow {
|
||||
0%, 100% { box-shadow: 0 0 4px rgba(34, 197, 94, 0.3); border-color: rgba(34, 197, 94, 0.5) !important; }
|
||||
50% { box-shadow: 0 0 12px rgba(34, 197, 94, 0.6); border-color: rgba(34, 197, 94, 0.9) !important; }
|
||||
}
|
||||
|
||||
.voice-send-active.voice-send-blink {
|
||||
animation: voice-send-flash 0.4s ease-out !important;
|
||||
}
|
||||
|
||||
@keyframes voice-send-flash {
|
||||
0% { background: rgba(34, 197, 94, 0.6) !important; box-shadow: 0 0 16px rgba(34, 197, 94, 0.8); }
|
||||
100% { background: transparent !important; box-shadow: none; opacity: 0; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.voice-send-active { animation: none; box-shadow: 0 0 6px rgba(34, 197, 94, 0.4); }
|
||||
.voice-send-active.voice-send-blink { animation: none; }
|
||||
}
|
||||
|
||||
.form-row input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
Reference in New Issue
Block a user