chore: bump version to 0.1494

This commit is contained in:
arkon
2026-02-13 06:02:12 +01:00
parent 7d2519cacb
commit 806750fe77
6 changed files with 226 additions and 32 deletions
+68 -30
View File
@@ -1237,6 +1237,9 @@ class ClaudemanApp {
this._inputQueueMaxBytes = 64 * 1024; // 64KB cap per session
this._connectionStatus = 'connected';
// Sequential input send chain — ensures keystroke ordering across async fetches
this._inputSendChain = Promise.resolve();
// Accessibility: Focus trap for modals
this.activeFocusTrap = null;
@@ -1538,38 +1541,24 @@ class ClaudemanApp {
// Handle keyboard input with batching for rapid keystrokes
this._pendingInput = '';
this._inputFlushTimeout = null;
this._inputFlushDelay = 16; // Flush at 60fps max
this._inputFlushDelay = 8; // Flush at ~120fps for snappy input
const flushInput = () => {
// Clear timeout immediately so new keystrokes can schedule fresh flushes.
// Previously this was set AFTER the await fetch(), which meant the stale
// timer ID blocked new keystrokes from scheduling — serializing all input
// behind the previous fetch's full network round-trip.
this._inputFlushTimeout = null;
const flushInput = async () => {
if (this._pendingInput && this.activeSessionId) {
const input = this._pendingInput;
const sessionId = this.activeSessionId;
this._pendingInput = '';
// Queue immediately if offline
if (!this.isOnline || this._connectionStatus === 'disconnected') {
this._enqueueInput(sessionId, input);
this._inputFlushTimeout = null;
return;
}
try {
const resp = await fetch(`/api/sessions/${sessionId}/input`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input })
});
if (!resp.ok) {
this._enqueueInput(sessionId, input);
} else {
// Clear pending hooks when user sends input (they've addressed the prompt)
this.clearPendingHooks(sessionId);
}
} catch {
this._enqueueInput(sessionId, input);
}
// Fire-and-forget: don't block the flush cycle waiting for network.
// This prevents keystroke serialization behind fetch RTT.
this._sendInputAsync(sessionId, input);
}
this._inputFlushTimeout = null;
};
this.terminal.onData((data) => {
@@ -1592,7 +1581,7 @@ class ClaudemanApp {
return;
}
// Batch regular input at 60fps
// Batch regular input at high frequency for responsiveness
if (!this._inputFlushTimeout) {
this._inputFlushTimeout = setTimeout(flushInput, this._inputFlushDelay);
}
@@ -2976,6 +2965,37 @@ class ClaudemanApp {
}
}
/**
* Send input to server without blocking the keystroke flush cycle.
* Uses a sequential promise chain to preserve character ordering
* across concurrent async fetches.
*/
_sendInputAsync(sessionId, input) {
// Queue immediately if offline
if (!this.isOnline || this._connectionStatus === 'disconnected') {
this._enqueueInput(sessionId, input);
return;
}
// Chain sends sequentially to preserve keystroke ordering
this._inputSendChain = this._inputSendChain.then(async () => {
try {
const resp = await fetch(`/api/sessions/${sessionId}/input`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input })
});
if (!resp.ok) {
this._enqueueInput(sessionId, input);
} else {
this.clearPendingHooks(sessionId);
}
} catch {
this._enqueueInput(sessionId, input);
}
});
}
_enqueueInput(sessionId, input) {
const existing = this._inputQueue.get(sessionId) || '';
let combined = existing + input;
@@ -3757,10 +3777,16 @@ class ClaudemanApp {
// Green fade overlay — Web Animations API for reliable GPU compositing.
// (CSS keyframe animations were invisible: black-on-black had zero contrast.)
const fadeCfg = this.loadAppSettingsFromStorage();
const fadeEnabled = fadeCfg.tabFadeEnabled ?? true;
const fadeInDur = fadeCfg.tabFadeInDuration ?? 250;
const fadeInEase = fadeCfg.tabFadeInEasing || 'ease-in';
const fadeOutDur = fadeCfg.tabFadeOutDuration ?? 120;
const fadeOutEase = fadeCfg.tabFadeOutEasing || 'ease-in';
const main = document.querySelector('.main');
let fade = null;
const fadeOutPromise = new Promise(resolve => {
if (main) {
if (main && fadeEnabled) {
const prev = main.querySelector('.tab-switch-fade');
if (prev) prev.remove();
fade = document.createElement('div');
@@ -3773,10 +3799,10 @@ class ClaudemanApp {
// Web Animations API — runs on compositor thread, bypasses containment issues
const anim = fade.animate(
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 120, easing: 'ease-out', fill: 'forwards' }
{ duration: fadeInDur, easing: fadeInEase, fill: 'forwards' }
);
anim.finished.then(resolve).catch(resolve);
setTimeout(resolve, 350); // safety fallback
setTimeout(resolve, fadeInDur + 100); // safety fallback
} else {
resolve();
}
@@ -3920,7 +3946,7 @@ class ClaudemanApp {
if (fade) {
const revealAnim = fade.animate(
[{ opacity: 1 }, { opacity: 0 }],
{ duration: 120, easing: 'ease-in', fill: 'forwards' }
{ duration: fadeOutDur, easing: fadeOutEase, fill: 'forwards' }
);
revealAnim.finished.then(() => fade.remove()).catch(() => fade.remove());
}
@@ -8986,6 +9012,12 @@ class ClaudemanApp {
document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? true;
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? true;
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? false;
// Tab switch animation settings
document.getElementById('appSettingsTabFadeEnabled').checked = settings.tabFadeEnabled ?? true;
document.getElementById('appSettingsTabFadeInDuration').value = settings.tabFadeInDuration ?? 250;
document.getElementById('appSettingsTabFadeInEasing').value = settings.tabFadeInEasing || 'ease-in';
document.getElementById('appSettingsTabFadeOutDuration').value = settings.tabFadeOutDuration ?? 120;
document.getElementById('appSettingsTabFadeOutEasing').value = settings.tabFadeOutEasing || 'ease-in';
// Claude CLI settings
const claudeModeSelect = document.getElementById('appSettingsClaudeMode');
const allowedToolsRow = document.getElementById('allowedToolsRow');
@@ -9111,6 +9143,12 @@ class ClaudemanApp {
subagentTrackingEnabled: document.getElementById('appSettingsSubagentTracking').checked,
subagentActiveTabOnly: document.getElementById('appSettingsSubagentActiveTabOnly').checked,
imageWatcherEnabled: document.getElementById('appSettingsImageWatcherEnabled').checked,
// Tab switch animation settings
tabFadeEnabled: document.getElementById('appSettingsTabFadeEnabled').checked,
tabFadeInDuration: parseInt(document.getElementById('appSettingsTabFadeInDuration').value) || 250,
tabFadeInEasing: document.getElementById('appSettingsTabFadeInEasing').value || 'ease-in',
tabFadeOutDuration: parseInt(document.getElementById('appSettingsTabFadeOutDuration').value) || 120,
tabFadeOutEasing: document.getElementById('appSettingsTabFadeOutEasing').value || 'ease-in',
// Claude CLI settings
claudeMode: document.getElementById('appSettingsClaudeMode').value,
allowedTools: document.getElementById('appSettingsAllowedTools').value.trim(),
+48
View File
@@ -726,6 +726,7 @@
</div>
<div class="modal-tabs">
<button class="modal-tab-btn active" data-tab="settings-display">Display</button>
<button class="modal-tab-btn" data-tab="settings-tab-switch">Tab-Switch</button>
<button class="modal-tab-btn" data-tab="settings-claude">Claude CLI</button>
<button class="modal-tab-btn" data-tab="settings-models">Models</button>
<button class="modal-tab-btn" data-tab="settings-paths">Paths</button>
@@ -825,6 +826,53 @@
</div>
</div>
</div>
<!-- Tab-Switch Tab -->
<div class="modal-tab-content hidden" id="settings-tab-switch">
<div class="settings-grid">
<div class="settings-section-header">Tab Switch Animation</div>
<div class="settings-item" title="Green fade overlay when switching session tabs">
<span class="settings-item-label">Enable</span>
<label class="switch switch-sm">
<input type="checkbox" id="appSettingsTabFadeEnabled" checked>
<span class="slider"></span>
</label>
</div>
<div class="settings-item" title="Fade-in duration in milliseconds">
<span class="settings-item-label">Fade In</span>
<div class="settings-item-actions">
<input type="number" id="appSettingsTabFadeInDuration" class="settings-item-input" value="250" min="50" max="1000" step="10">
<span style="font-size: 0.65rem; color: var(--text-muted);">ms</span>
</div>
</div>
<div class="settings-item" title="Fade-in easing curve">
<span class="settings-item-label">Fade In Easing</span>
<select id="appSettingsTabFadeInEasing" class="settings-item-input" style="width: 100px; padding: 0.25rem 0.4rem; font-size: 0.7rem; background: var(--bg-input); border: 1px solid var(--border); border-radius: 3px; color: var(--text);">
<option value="linear">linear</option>
<option value="ease">ease</option>
<option value="ease-in">ease-in</option>
<option value="ease-out">ease-out</option>
<option value="ease-in-out">ease-in-out</option>
</select>
</div>
<div class="settings-item" title="Fade-out (reveal) duration in milliseconds">
<span class="settings-item-label">Fade Out</span>
<div class="settings-item-actions">
<input type="number" id="appSettingsTabFadeOutDuration" class="settings-item-input" value="120" min="50" max="1000" step="10">
<span style="font-size: 0.65rem; color: var(--text-muted);">ms</span>
</div>
</div>
<div class="settings-item" title="Fade-out easing curve">
<span class="settings-item-label">Fade Out Easing</span>
<select id="appSettingsTabFadeOutEasing" class="settings-item-input" style="width: 100px; padding: 0.25rem 0.4rem; font-size: 0.7rem; background: var(--bg-input); border: 1px solid var(--border); border-radius: 3px; color: var(--text);">
<option value="linear">linear</option>
<option value="ease">ease</option>
<option value="ease-in">ease-in</option>
<option value="ease-out">ease-out</option>
<option value="ease-in-out">ease-in-out</option>
</select>
</div>
</div>
</div>
<!-- Claude CLI Tab -->
<div class="modal-tab-content hidden" id="settings-claude">
<div class="form-row">
+9
View File
@@ -2765,6 +2765,15 @@ body {
.modal-content.modal-lg {
max-width: 520px;
max-height: 85vh;
display: flex;
flex-direction: column;
}
.modal-content.modal-lg > .modal-body {
overflow-y: auto;
flex: 1;
min-height: 0;
}
/* Mobile Case Picker - Base Styles */