mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
chore: bump version to 0.1633
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.1632 (must match `package.json`)
|
**Version**: 0.1633 (must match `package.json`)
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"name": "claudeman",
|
||||||
"version": "0.1632",
|
"version": "0.1633",
|
||||||
"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",
|
||||||
|
|||||||
+241
-1
@@ -589,6 +589,246 @@ const SwipeHandler = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Voice Input (Web Speech API)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VoiceInput - Speech-to-text using the Web Speech API.
|
||||||
|
* Toggle mode: tap mic to start, tap again to stop. Auto-stops after silence.
|
||||||
|
* Shows interim transcription in a floating preview overlay.
|
||||||
|
* Inserts final text into the active session (user presses Enter to submit).
|
||||||
|
*/
|
||||||
|
const VoiceInput = {
|
||||||
|
recognition: null,
|
||||||
|
isRecording: false,
|
||||||
|
supported: false,
|
||||||
|
silenceTimeout: null,
|
||||||
|
previewEl: null,
|
||||||
|
_lastTranscript: '',
|
||||||
|
_stabilityTimer: null,
|
||||||
|
_accumulatedFinal: '',
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
this.supported = !!SR;
|
||||||
|
if (!this.supported) return;
|
||||||
|
|
||||||
|
this.recognition = new SR();
|
||||||
|
this.recognition.continuous = false;
|
||||||
|
this.recognition.interimResults = true;
|
||||||
|
this.recognition.lang = 'en-US';
|
||||||
|
this.recognition.maxAlternatives = 1;
|
||||||
|
|
||||||
|
this.recognition.onresult = (e) => this._onResult(e);
|
||||||
|
this.recognition.onerror = (e) => this._onError(e);
|
||||||
|
this.recognition.onend = () => this._onEnd();
|
||||||
|
|
||||||
|
// Show voice buttons now that we know it's supported
|
||||||
|
this._showButtons();
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isRecording) {
|
||||||
|
this.stop();
|
||||||
|
} else {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (!this.supported || this.isRecording) return;
|
||||||
|
if (!app.activeSessionId) {
|
||||||
|
app.showToast('No active session', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isRecording = true;
|
||||||
|
this._accumulatedFinal = '';
|
||||||
|
this._lastTranscript = '';
|
||||||
|
this._updateButtons('recording');
|
||||||
|
this._showPreview('Listening...');
|
||||||
|
try {
|
||||||
|
this.recognition.start();
|
||||||
|
} catch (_e) {
|
||||||
|
// Already started — ignore
|
||||||
|
}
|
||||||
|
this._resetSilenceTimeout();
|
||||||
|
// Haptic feedback on mobile
|
||||||
|
if (navigator.vibrate) navigator.vibrate(50);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (!this.isRecording) return;
|
||||||
|
this.isRecording = false;
|
||||||
|
clearTimeout(this.silenceTimeout);
|
||||||
|
clearTimeout(this._stabilityTimer);
|
||||||
|
this.silenceTimeout = null;
|
||||||
|
this._stabilityTimer = null;
|
||||||
|
this._updateButtons('idle');
|
||||||
|
this._hidePreview();
|
||||||
|
try {
|
||||||
|
this.recognition.stop();
|
||||||
|
} catch (_e) {
|
||||||
|
// Already stopped — ignore
|
||||||
|
}
|
||||||
|
// Haptic feedback on mobile
|
||||||
|
if (navigator.vibrate) navigator.vibrate([30, 50, 30]);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onResult(event) {
|
||||||
|
this._resetSilenceTimeout();
|
||||||
|
let interim = '';
|
||||||
|
let finalText = '';
|
||||||
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||||
|
const transcript = event.results[i][0].transcript;
|
||||||
|
if (event.results[i].isFinal) {
|
||||||
|
finalText += transcript;
|
||||||
|
} else {
|
||||||
|
interim += transcript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalText) {
|
||||||
|
this._accumulatedFinal += finalText;
|
||||||
|
this._hidePreview();
|
||||||
|
this._insertText(this._accumulatedFinal);
|
||||||
|
this.stop();
|
||||||
|
} else if (interim) {
|
||||||
|
const display = this._accumulatedFinal + interim;
|
||||||
|
this._showPreview(display);
|
||||||
|
// iOS Safari workaround: isFinal is always false.
|
||||||
|
// Detect when interim results stop changing for 750ms → treat as final.
|
||||||
|
this._iosStabilityCheck(interim);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onError(event) {
|
||||||
|
const wasRecording = this.isRecording;
|
||||||
|
this.stop();
|
||||||
|
if (!wasRecording) return;
|
||||||
|
|
||||||
|
switch (event.error) {
|
||||||
|
case 'not-allowed':
|
||||||
|
app.showToast('Microphone access denied. Check browser settings.', 'error');
|
||||||
|
break;
|
||||||
|
case 'no-speech':
|
||||||
|
// Silent — auto-stop is enough feedback
|
||||||
|
break;
|
||||||
|
case 'network':
|
||||||
|
app.showToast('Voice input requires internet connection.', 'error');
|
||||||
|
break;
|
||||||
|
case 'aborted':
|
||||||
|
// User cancelled — no message needed
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
app.showToast('Voice input error: ' + event.error, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEnd() {
|
||||||
|
// Recognition ended (browser auto-stopped or we called stop())
|
||||||
|
if (this.isRecording) {
|
||||||
|
// Ended unexpectedly while still recording — finalize any accumulated text
|
||||||
|
if (this._accumulatedFinal) {
|
||||||
|
this._insertText(this._accumulatedFinal);
|
||||||
|
}
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_insertText(text) {
|
||||||
|
if (!app.activeSessionId || !text.trim()) return;
|
||||||
|
// Send text to session input (user presses Enter to submit)
|
||||||
|
app.sendInput(text.trim());
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetSilenceTimeout() {
|
||||||
|
clearTimeout(this.silenceTimeout);
|
||||||
|
this.silenceTimeout = setTimeout(() => {
|
||||||
|
if (this.isRecording) {
|
||||||
|
// Finalize any accumulated text before stopping
|
||||||
|
if (this._accumulatedFinal) {
|
||||||
|
this._insertText(this._accumulatedFinal);
|
||||||
|
}
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
|
||||||
|
_iosStabilityCheck(transcript) {
|
||||||
|
if (transcript !== this._lastTranscript) {
|
||||||
|
this._lastTranscript = transcript;
|
||||||
|
clearTimeout(this._stabilityTimer);
|
||||||
|
this._stabilityTimer = setTimeout(() => {
|
||||||
|
if (this.isRecording) {
|
||||||
|
const finalText = this._accumulatedFinal + transcript;
|
||||||
|
this._hidePreview();
|
||||||
|
this._insertText(finalText);
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
}, 750);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_showPreview(text) {
|
||||||
|
if (!this.previewEl) {
|
||||||
|
this.previewEl = document.createElement('div');
|
||||||
|
this.previewEl.className = 'voice-preview';
|
||||||
|
this.previewEl.setAttribute('aria-live', 'polite');
|
||||||
|
document.body.appendChild(this.previewEl);
|
||||||
|
}
|
||||||
|
this.previewEl.textContent = text || 'Listening...';
|
||||||
|
this.previewEl.style.display = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
_hidePreview() {
|
||||||
|
if (this.previewEl) {
|
||||||
|
this.previewEl.style.display = 'none';
|
||||||
|
this.previewEl.textContent = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateButtons(state) {
|
||||||
|
const isRecording = state === 'recording';
|
||||||
|
// Desktop button
|
||||||
|
const desktopBtn = document.getElementById('voiceInputBtn');
|
||||||
|
if (desktopBtn) {
|
||||||
|
desktopBtn.classList.toggle('recording', isRecording);
|
||||||
|
desktopBtn.setAttribute('aria-pressed', String(isRecording));
|
||||||
|
desktopBtn.setAttribute('aria-label', isRecording ? 'Stop voice input' : 'Start voice input');
|
||||||
|
desktopBtn.title = isRecording ? 'Stop voice input (Ctrl+Shift+V)' : 'Voice input (Ctrl+Shift+V)';
|
||||||
|
}
|
||||||
|
// Mobile button (inside accessory bar)
|
||||||
|
const mobileBtn = document.querySelector('[data-action="voice"]');
|
||||||
|
if (mobileBtn) {
|
||||||
|
mobileBtn.classList.toggle('recording', isRecording);
|
||||||
|
mobileBtn.setAttribute('aria-pressed', String(isRecording));
|
||||||
|
mobileBtn.setAttribute('aria-label', isRecording ? 'Stop voice input' : 'Start voice input');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_showButtons() {
|
||||||
|
// Desktop: show the toolbar button
|
||||||
|
const desktopBtn = document.getElementById('voiceInputBtn');
|
||||||
|
if (desktopBtn) desktopBtn.style.display = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Cleanup on SSE reconnect or page unload */
|
||||||
|
cleanup() {
|
||||||
|
if (this.isRecording) this.stop();
|
||||||
|
if (this.previewEl) {
|
||||||
|
this.previewEl.remove();
|
||||||
|
this.previewEl = null;
|
||||||
|
}
|
||||||
|
clearTimeout(this.silenceTimeout);
|
||||||
|
clearTimeout(this._stabilityTimer);
|
||||||
|
this.silenceTimeout = null;
|
||||||
|
this._stabilityTimer = null;
|
||||||
|
this._accumulatedFinal = '';
|
||||||
|
this._lastTranscript = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Mobile Keyboard Accessory Bar
|
// Mobile Keyboard Accessory Bar
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1010,7 +1250,7 @@ function _zl_makeLine(text, leftPx, topPx, widthPx, cellH, cellW, font, terminal
|
|||||||
for (const ch of text) {
|
for (const ch of text) {
|
||||||
const charWidth = _zl_charCellWidth(ch, terminal);
|
const charWidth = _zl_charCellWidth(ch, terminal);
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.style.cssText = "position:absolute;display:inline-block;text-align:center;pointer-events:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:geometricPrecision;font-feature-settings:'liga' 0,'calt' 0";
|
span.style.cssText = "position:absolute;display:inline-block;text-align:center;pointer-events:none;font-feature-settings:'liga' 0,'calt' 0";
|
||||||
span.style.left = (colOffset * cellW) + 'px';
|
span.style.left = (colOffset * cellW) + 'px';
|
||||||
span.style.width = (charWidth * cellW) + 'px';
|
span.style.width = (charWidth * cellW) + 'px';
|
||||||
span.style.fontFamily = font.fontFamily;
|
span.style.fontFamily = font.fontFamily;
|
||||||
|
|||||||
Reference in New Issue
Block a user