mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39: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`)
|
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`
|
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
|
## Project Overview
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"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",
|
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
+100
-18
@@ -603,6 +603,7 @@ const DeepgramProvider = {
|
|||||||
_mediaRecorder: null,
|
_mediaRecorder: null,
|
||||||
_stream: null,
|
_stream: null,
|
||||||
_silenceTimeout: null,
|
_silenceTimeout: null,
|
||||||
|
_keepAliveInterval: null,
|
||||||
_onResult: null,
|
_onResult: null,
|
||||||
_onError: null,
|
_onError: null,
|
||||||
_onEnd: null,
|
_onEnd: null,
|
||||||
@@ -632,11 +633,22 @@ const DeepgramProvider = {
|
|||||||
// Notify caller so it can set up audio level meter
|
// Notify caller so it can set up audio level meter
|
||||||
opts.onStream?.(this._stream);
|
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({
|
const params = new URLSearchParams({
|
||||||
model: 'nova-3',
|
model: 'nova-3',
|
||||||
smart_format: 'true',
|
smart_format: 'false',
|
||||||
punctuate: 'true',
|
punctuate: 'false',
|
||||||
interim_results: 'true',
|
interim_results: 'true',
|
||||||
utterance_end_ms: '1500',
|
utterance_end_ms: '1500',
|
||||||
vad_events: 'true',
|
vad_events: 'true',
|
||||||
@@ -649,14 +661,20 @@ const DeepgramProvider = {
|
|||||||
if (opts.keyterms?.length) {
|
if (opts.keyterms?.length) {
|
||||||
for (const term of opts.keyterms) {
|
for (const term of opts.keyterms) {
|
||||||
const trimmed = term.trim();
|
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}`;
|
const wsUrl = `wss://api.deepgram.com/v1/listen?${params}`;
|
||||||
try {
|
try {
|
||||||
this._ws = new WebSocket(wsUrl, ['token', opts.apiKey]);
|
this._ws = new WebSocket(wsUrl, ['token', apiKey]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._onError?.('Failed to connect to Deepgram: ' + err.message);
|
this._onError?.('Failed to connect to Deepgram: ' + err.message);
|
||||||
this._cleanup();
|
this._cleanup();
|
||||||
@@ -664,7 +682,14 @@ const DeepgramProvider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._ws.onopen = () => {
|
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();
|
this._startRecording();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -690,8 +715,13 @@ const DeepgramProvider = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this._ws.onclose = (event) => {
|
this._ws.onclose = (event) => {
|
||||||
|
clearInterval(this._keepAliveInterval);
|
||||||
|
this._keepAliveInterval = null;
|
||||||
if (event.code === 1008) {
|
if (event.code === 1008) {
|
||||||
this._onError?.('Authentication failed. Check your Deepgram API key in Settings > Voice.');
|
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) {
|
} else if (event.code !== 1000) {
|
||||||
this._onError?.('Deepgram connection closed: ' + (event.reason || `code ${event.code}`));
|
this._onError?.('Deepgram connection closed: ' + (event.reason || `code ${event.code}`));
|
||||||
}
|
}
|
||||||
@@ -703,17 +733,7 @@ const DeepgramProvider = {
|
|||||||
_startRecording() {
|
_startRecording() {
|
||||||
if (!this._stream || !this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
if (!this._stream || !this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
// Pick best supported MIME type
|
const recorderOpts = this._selectedMime ? { mimeType: this._selectedMime } : {};
|
||||||
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 } : {};
|
|
||||||
try {
|
try {
|
||||||
this._mediaRecorder = new MediaRecorder(this._stream, recorderOpts);
|
this._mediaRecorder = new MediaRecorder(this._stream, recorderOpts);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -752,6 +772,8 @@ const DeepgramProvider = {
|
|||||||
stop() {
|
stop() {
|
||||||
clearTimeout(this._silenceTimeout);
|
clearTimeout(this._silenceTimeout);
|
||||||
this._silenceTimeout = null;
|
this._silenceTimeout = null;
|
||||||
|
clearInterval(this._keepAliveInterval);
|
||||||
|
this._keepAliveInterval = null;
|
||||||
this._stopRecording();
|
this._stopRecording();
|
||||||
if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
if (this._ws && this._ws.readyState === WebSocket.OPEN) {
|
||||||
try { this._ws.close(1000); } catch (_e) { /* ignore */ }
|
try { this._ws.close(1000); } catch (_e) { /* ignore */ }
|
||||||
@@ -763,6 +785,7 @@ const DeepgramProvider = {
|
|||||||
this._ws = null;
|
this._ws = null;
|
||||||
this._mediaRecorder = null;
|
this._mediaRecorder = null;
|
||||||
this._stream = null;
|
this._stream = null;
|
||||||
|
this._selectedMime = null;
|
||||||
this._onResult = null;
|
this._onResult = null;
|
||||||
this._onError = null;
|
this._onError = null;
|
||||||
this._onEnd = null;
|
this._onEnd = null;
|
||||||
@@ -1092,10 +1115,68 @@ const VoiceInput = {
|
|||||||
} else {
|
} else {
|
||||||
app.sendInput(trimmed).catch(() => {});
|
app.sendInput(trimmed).catch(() => {});
|
||||||
}
|
}
|
||||||
|
this._showVoiceSendBtn();
|
||||||
setTimeout(() => { if (app.terminal) app.terminal.focus(); }, 150);
|
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 */
|
/** Show an editable compose overlay so the user can review/edit before sending */
|
||||||
_showComposeOverlay(text) {
|
_showComposeOverlay(text) {
|
||||||
document.querySelector('.voice-compose-overlay')?.remove();
|
document.querySelector('.voice-compose-overlay')?.remove();
|
||||||
@@ -1315,6 +1396,7 @@ const VoiceInput = {
|
|||||||
/** Cleanup on SSE reconnect or page unload */
|
/** Cleanup on SSE reconnect or page unload */
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if (this.isRecording) this.stop();
|
if (this.isRecording) this.stop();
|
||||||
|
this._hideVoiceSendBtn();
|
||||||
DeepgramProvider._cleanup();
|
DeepgramProvider._cleanup();
|
||||||
this.recognition = null;
|
this.recognition = null;
|
||||||
this._activeProvider = 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"] {
|
.form-row input[type="password"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user