mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
perf: optimize hot paths across server, session, and frontend
- Pre-compile regex patterns (ANSI escape, token parsing) in session.ts - Optimize SSE broadcast to serialize JSON once for all clients - Add DOM element caching ($() helper) to avoid repeated getElementById - Throttle resize observer with dimension change detection - Use array.join() for session tab HTML building - Replace DOM-based escapeHtml with regex replacement - Cache toast container reference Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+15
-8
@@ -26,6 +26,10 @@ const LINE_BUFFER_FLUSH_INTERVAL = 100;
|
|||||||
// ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences
|
// ^[[I (focus in), ^[[O (focus out), and the enable/disable sequences
|
||||||
const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
|
const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
|
||||||
|
|
||||||
|
// Pre-compiled regex patterns for performance (avoid re-compilation on each call)
|
||||||
|
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||||
|
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
|
||||||
|
|
||||||
export interface ClaudeMessage {
|
export interface ClaudeMessage {
|
||||||
type: 'system' | 'assistant' | 'user' | 'result';
|
type: 'system' | 'assistant' | 'user' | 'result';
|
||||||
subtype?: string;
|
subtype?: string;
|
||||||
@@ -658,8 +662,8 @@ export class Session extends EventEmitter {
|
|||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
// Remove ANSI escape codes for JSON parsing
|
// Remove ANSI escape codes for JSON parsing (use pre-compiled pattern)
|
||||||
const cleanLine = trimmed.replace(/\x1b\[[0-9;]*m/g, '');
|
const cleanLine = trimmed.replace(ANSI_ESCAPE_PATTERN, '');
|
||||||
|
|
||||||
if (cleanLine.startsWith('{') && cleanLine.endsWith('}')) {
|
if (cleanLine.startsWith('{') && cleanLine.endsWith('}')) {
|
||||||
try {
|
try {
|
||||||
@@ -716,12 +720,12 @@ export class Session extends EventEmitter {
|
|||||||
// Parse token count from Claude's status line in interactive mode
|
// Parse token count from Claude's status line in interactive mode
|
||||||
// Matches patterns like "123.4k tokens", "5234 tokens", "1.2M tokens"
|
// Matches patterns like "123.4k tokens", "5234 tokens", "1.2M tokens"
|
||||||
private parseTokensFromStatusLine(data: string): void {
|
private parseTokensFromStatusLine(data: string): void {
|
||||||
// Remove ANSI escape codes for cleaner parsing
|
// Remove ANSI escape codes for cleaner parsing (use pre-compiled pattern)
|
||||||
const cleanData = data.replace(/\x1b\[[0-9;]*m/g, '');
|
const cleanData = data.replace(ANSI_ESCAPE_PATTERN, '');
|
||||||
|
|
||||||
// Match patterns: "123.4k tokens", "5234 tokens", "1.2M tokens"
|
// Match patterns: "123.4k tokens", "5234 tokens", "1.2M tokens"
|
||||||
// The status line typically shows total tokens like "1.2k tokens" near the prompt
|
// The status line typically shows total tokens like "1.2k tokens" near the prompt
|
||||||
const tokenMatch = cleanData.match(/(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/);
|
const tokenMatch = cleanData.match(TOKEN_PATTERN);
|
||||||
|
|
||||||
if (tokenMatch) {
|
if (tokenMatch) {
|
||||||
let tokenCount = parseFloat(tokenMatch[1]);
|
let tokenCount = parseFloat(tokenMatch[1]);
|
||||||
@@ -863,10 +867,13 @@ export class Session extends EventEmitter {
|
|||||||
this._currentTaskId = null;
|
this._currentTaskId = null;
|
||||||
|
|
||||||
// Kill the associated screen session if requested
|
// Kill the associated screen session if requested
|
||||||
if (this._screenSession && this._screenManager && killScreen) {
|
if (killScreen && this._screenManager) {
|
||||||
|
// Try to kill screen even if _screenSession is not set (e.g., restored sessions)
|
||||||
try {
|
try {
|
||||||
await this._screenManager.killScreen(this.id);
|
const killed = await this._screenManager.killScreen(this.id);
|
||||||
console.log('[Session] Killed screen session:', this._screenSession.screenName);
|
if (killed) {
|
||||||
|
console.log('[Session] Killed screen session for:', this.id);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Session] Failed to kill screen session:', err);
|
console.error('[Session] Failed to kill screen session:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
+125
-47
@@ -29,9 +29,20 @@ class ClaudemanApp {
|
|||||||
// System stats polling
|
// System stats polling
|
||||||
this.systemStatsInterval = null;
|
this.systemStatsInterval = null;
|
||||||
|
|
||||||
|
// DOM element cache for performance (avoid repeated getElementById calls)
|
||||||
|
this._elemCache = {};
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cached element getter - avoids repeated DOM queries
|
||||||
|
$(id) {
|
||||||
|
if (!this._elemCache[id]) {
|
||||||
|
this._elemCache[id] = document.getElementById(id);
|
||||||
|
}
|
||||||
|
return this._elemCache[id];
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.initTerminal();
|
this.initTerminal();
|
||||||
this.loadFontSize();
|
this.loadFontSize();
|
||||||
@@ -92,24 +103,36 @@ class ClaudemanApp {
|
|||||||
// Welcome message
|
// Welcome message
|
||||||
this.showWelcome();
|
this.showWelcome();
|
||||||
|
|
||||||
// Handle resize
|
// Handle resize with throttling for performance
|
||||||
window.addEventListener('resize', () => this.fitAddon && this.fitAddon.fit());
|
this._resizeTimeout = null;
|
||||||
|
this._lastResizeDims = null;
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const throttledResize = () => {
|
||||||
if (this.fitAddon) {
|
if (this._resizeTimeout) return;
|
||||||
this.fitAddon.fit();
|
this._resizeTimeout = setTimeout(() => {
|
||||||
if (this.activeSessionId) {
|
this._resizeTimeout = null;
|
||||||
const dims = this.fitAddon.proposeDimensions();
|
if (this.fitAddon) {
|
||||||
if (dims) {
|
this.fitAddon.fit();
|
||||||
fetch(`/api/sessions/${this.activeSessionId}/resize`, {
|
if (this.activeSessionId) {
|
||||||
method: 'POST',
|
const dims = this.fitAddon.proposeDimensions();
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// Only send resize if dimensions actually changed
|
||||||
body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
|
if (dims && (!this._lastResizeDims ||
|
||||||
});
|
dims.cols !== this._lastResizeDims.cols ||
|
||||||
|
dims.rows !== this._lastResizeDims.rows)) {
|
||||||
|
this._lastResizeDims = { cols: dims.cols, rows: dims.rows };
|
||||||
|
fetch(`/api/sessions/${this.activeSessionId}/resize`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, 100); // Throttle to 100ms
|
||||||
});
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', throttledResize);
|
||||||
|
const resizeObserver = new ResizeObserver(throttledResize);
|
||||||
resizeObserver.observe(container);
|
resizeObserver.observe(container);
|
||||||
|
|
||||||
// Handle keyboard input
|
// Handle keyboard input
|
||||||
@@ -291,19 +314,23 @@ class ClaudemanApp {
|
|||||||
|
|
||||||
this.eventSource.addEventListener('session:idle', (e) => {
|
this.eventSource.addEventListener('session:idle', (e) => {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
|
console.log('[DEBUG] session:idle event for:', data.id);
|
||||||
const session = this.sessions.get(data.id);
|
const session = this.sessions.get(data.id);
|
||||||
if (session) {
|
if (session) {
|
||||||
session.status = 'idle';
|
session.status = 'idle';
|
||||||
this.renderSessionTabs();
|
this.renderSessionTabs();
|
||||||
|
this.sendPendingCtrlL(data.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventSource.addEventListener('session:working', (e) => {
|
this.eventSource.addEventListener('session:working', (e) => {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
|
console.log('[DEBUG] session:working event for:', data.id);
|
||||||
const session = this.sessions.get(data.id);
|
const session = this.sessions.get(data.id);
|
||||||
if (session) {
|
if (session) {
|
||||||
session.status = 'busy';
|
session.status = 'busy';
|
||||||
this.renderSessionTabs();
|
this.renderSessionTabs();
|
||||||
|
this.sendPendingCtrlL(data.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -512,10 +539,10 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_renderSessionTabsImmediate() {
|
_renderSessionTabsImmediate() {
|
||||||
const container = document.getElementById('sessionTabs');
|
const container = this.$('sessionTabs');
|
||||||
|
|
||||||
// Build tabs HTML
|
// Build tabs HTML using array for better string concatenation performance
|
||||||
let html = '';
|
const parts = [];
|
||||||
for (const [id, session] of this.sessions) {
|
for (const [id, session] of this.sessions) {
|
||||||
const isActive = id === this.activeSessionId;
|
const isActive = id === this.activeSessionId;
|
||||||
const status = session.status || 'idle';
|
const status = session.status || 'idle';
|
||||||
@@ -524,19 +551,17 @@ class ClaudemanApp {
|
|||||||
const taskStats = session.taskStats || { running: 0, total: 0 };
|
const taskStats = session.taskStats || { running: 0, total: 0 };
|
||||||
const hasRunningTasks = taskStats.running > 0;
|
const hasRunningTasks = taskStats.running > 0;
|
||||||
|
|
||||||
html += `
|
parts.push(`<div class="session-tab ${isActive ? 'active' : ''}" data-id="${id}" onclick="app.selectSession('${id}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${id}')">
|
||||||
<div class="session-tab ${isActive ? 'active' : ''}" data-id="${id}" onclick="app.selectSession('${id}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${id}')">
|
|
||||||
<span class="tab-status ${status}"></span>
|
<span class="tab-status ${status}"></span>
|
||||||
${mode === 'shell' ? '<span class="tab-mode shell">sh</span>' : ''}
|
${mode === 'shell' ? '<span class="tab-mode shell">sh</span>' : ''}
|
||||||
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
|
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
|
||||||
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()">${taskStats.running}</span>` : ''}
|
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()">${taskStats.running}</span>` : ''}
|
||||||
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${id}')" title="Session options">⚙</span>
|
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${id}')" title="Session options">⚙</span>
|
||||||
<span class="tab-close" onclick="event.stopPropagation(); app.requestCloseSession('${id}')">×</span>
|
<span class="tab-close" onclick="event.stopPropagation(); app.requestCloseSession('${id}')">×</span>
|
||||||
</div>
|
</div>`);
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = parts.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSessionName(session) {
|
getSessionName(session) {
|
||||||
@@ -597,7 +622,8 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update task panel if open
|
// Update task panel if open
|
||||||
if (document.getElementById('taskPanel').classList.contains('open')) {
|
const taskPanel = document.getElementById('taskPanel');
|
||||||
|
if (taskPanel && taskPanel.classList.contains('open')) {
|
||||||
this.renderTaskPanel();
|
this.renderTaskPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,12 +850,16 @@ class ClaudemanApp {
|
|||||||
// Send resize to the new session
|
// Send resize to the new session
|
||||||
const dims = this.fitAddon.proposeDimensions();
|
const dims = this.fitAddon.proposeDimensions();
|
||||||
if (dims) {
|
if (dims) {
|
||||||
fetch(`/api/sessions/${firstSessionId}/resize`, {
|
await fetch(`/api/sessions/${firstSessionId}/resize`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
|
body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Mark this session as needing Ctrl+L fix once Claude is running
|
||||||
|
this.pendingCtrlL = this.pendingCtrlL || new Set();
|
||||||
|
this.pendingCtrlL.add(firstSessionId);
|
||||||
|
console.log('[DEBUG] Marked session for pending Ctrl+L:', firstSessionId);
|
||||||
|
|
||||||
this.loadQuickStartCases();
|
this.loadQuickStartCases();
|
||||||
}
|
}
|
||||||
@@ -929,7 +959,7 @@ class ClaudemanApp {
|
|||||||
// ========== Respawn Banner ==========
|
// ========== Respawn Banner ==========
|
||||||
|
|
||||||
showRespawnBanner() {
|
showRespawnBanner() {
|
||||||
document.getElementById('respawnBanner').style.display = 'flex';
|
this.$('respawnBanner').style.display = 'flex';
|
||||||
// Also show timer if there's a timed respawn
|
// Also show timer if there's a timed respawn
|
||||||
if (this.activeSessionId && this.respawnTimers[this.activeSessionId]) {
|
if (this.activeSessionId && this.respawnTimers[this.activeSessionId]) {
|
||||||
this.showRespawnTimer();
|
this.showRespawnTimer();
|
||||||
@@ -942,16 +972,16 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hideRespawnBanner() {
|
hideRespawnBanner() {
|
||||||
document.getElementById('respawnBanner').style.display = 'none';
|
this.$('respawnBanner').style.display = 'none';
|
||||||
this.hideRespawnTimer();
|
this.hideRespawnTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRespawnBanner(state) {
|
updateRespawnBanner(state) {
|
||||||
document.getElementById('respawnState').textContent = state.replace(/_/g, ' ');
|
this.$('respawnState').textContent = state.replace(/_/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
showRespawnTimer() {
|
showRespawnTimer() {
|
||||||
const timerEl = document.getElementById('respawnTimer');
|
const timerEl = this.$('respawnTimer');
|
||||||
timerEl.style.display = '';
|
timerEl.style.display = '';
|
||||||
this.updateRespawnTimer();
|
this.updateRespawnTimer();
|
||||||
// Update every second
|
// Update every second
|
||||||
@@ -960,7 +990,7 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hideRespawnTimer() {
|
hideRespawnTimer() {
|
||||||
document.getElementById('respawnTimer').style.display = 'none';
|
this.$('respawnTimer').style.display = 'none';
|
||||||
if (this.respawnTimerInterval) {
|
if (this.respawnTimerInterval) {
|
||||||
clearInterval(this.respawnTimerInterval);
|
clearInterval(this.respawnTimerInterval);
|
||||||
this.respawnTimerInterval = null;
|
this.respawnTimerInterval = null;
|
||||||
@@ -978,17 +1008,17 @@ class ClaudemanApp {
|
|||||||
const remaining = Math.max(0, timer.endAt - now);
|
const remaining = Math.max(0, timer.endAt - now);
|
||||||
|
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
document.getElementById('respawnTimer').textContent = 'Time up';
|
this.$('respawnTimer').textContent = 'Time up';
|
||||||
delete this.respawnTimers[this.activeSessionId];
|
delete this.respawnTimers[this.activeSessionId];
|
||||||
this.hideRespawnTimer();
|
this.hideRespawnTimer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('respawnTimer').textContent = this.formatTime(remaining);
|
this.$('respawnTimer').textContent = this.formatTime(remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRespawnTokens(totalTokens) {
|
updateRespawnTokens(totalTokens) {
|
||||||
const tokensEl = document.getElementById('respawnTokens');
|
const tokensEl = this.$('respawnTokens');
|
||||||
if (totalTokens > 0) {
|
if (totalTokens > 0) {
|
||||||
tokensEl.style.display = '';
|
tokensEl.style.display = '';
|
||||||
tokensEl.textContent = `${(totalTokens / 1000).toFixed(1)}k tokens`;
|
tokensEl.textContent = `${(totalTokens / 1000).toFixed(1)}k tokens`;
|
||||||
@@ -1044,6 +1074,40 @@ class ClaudemanApp {
|
|||||||
this.terminal.clear();
|
this.terminal.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send Ctrl+L to fix display for newly created sessions once Claude is running
|
||||||
|
sendPendingCtrlL(sessionId) {
|
||||||
|
console.log('[DEBUG] sendPendingCtrlL called for:', sessionId, 'pending:', this.pendingCtrlL ? [...this.pendingCtrlL] : 'none');
|
||||||
|
if (!this.pendingCtrlL || !this.pendingCtrlL.has(sessionId)) {
|
||||||
|
console.log('[DEBUG] No pending Ctrl+L for this session');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.pendingCtrlL.delete(sessionId);
|
||||||
|
|
||||||
|
// Only send if this is the active session
|
||||||
|
if (sessionId !== this.activeSessionId) {
|
||||||
|
console.log('[DEBUG] Not active session, skipping Ctrl+L');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DEBUG] Sending resize + Ctrl+L for session:', sessionId);
|
||||||
|
// Send resize + Ctrl+L to fix the display
|
||||||
|
const dims = this.fitAddon.proposeDimensions();
|
||||||
|
if (dims) {
|
||||||
|
fetch(`/api/sessions/${sessionId}/resize`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ cols: dims.cols, rows: dims.rows })
|
||||||
|
}).then(() => {
|
||||||
|
console.log('[DEBUG] Resize sent, now sending Ctrl+L');
|
||||||
|
fetch(`/api/sessions/${sessionId}/input`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ input: '\x0c' })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async copyTerminal() {
|
async copyTerminal() {
|
||||||
try {
|
try {
|
||||||
const buffer = this.terminal.buffer.active;
|
const buffer = this.terminal.buffer.active;
|
||||||
@@ -1151,7 +1215,7 @@ class ClaudemanApp {
|
|||||||
});
|
});
|
||||||
this.totalTokens = total;
|
this.totalTokens = total;
|
||||||
const display = total >= 1000 ? `${(total / 1000).toFixed(1)}k` : total;
|
const display = total >= 1000 ? `${(total / 1000).toFixed(1)}k` : total;
|
||||||
document.getElementById('headerTokens').textContent = `${display} tokens`;
|
this.$('headerTokens').textContent = `${display} tokens`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Session Options Modal ==========
|
// ========== Session Options Modal ==========
|
||||||
@@ -1813,18 +1877,24 @@ class ClaudemanApp {
|
|||||||
|
|
||||||
// ========== Toast ==========
|
// ========== Toast ==========
|
||||||
|
|
||||||
|
// Cached toast container for performance
|
||||||
|
_toastContainer = null;
|
||||||
|
|
||||||
showToast(message, type = 'info') {
|
showToast(message, type = 'info') {
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.className = `toast toast-${type}`;
|
toast.className = `toast toast-${type}`;
|
||||||
toast.textContent = message;
|
toast.textContent = message;
|
||||||
|
|
||||||
let container = document.querySelector('.toast-container');
|
// Cache toast container reference
|
||||||
if (!container) {
|
if (!this._toastContainer) {
|
||||||
container = document.createElement('div');
|
this._toastContainer = document.querySelector('.toast-container');
|
||||||
container.className = 'toast-container';
|
if (!this._toastContainer) {
|
||||||
document.body.appendChild(container);
|
this._toastContainer = document.createElement('div');
|
||||||
|
this._toastContainer.className = 'toast-container';
|
||||||
|
document.body.appendChild(this._toastContainer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
container.appendChild(toast);
|
this._toastContainer.appendChild(toast);
|
||||||
|
|
||||||
requestAnimationFrame(() => toast.classList.add('show'));
|
requestAnimationFrame(() => toast.classList.add('show'));
|
||||||
|
|
||||||
@@ -1857,10 +1927,10 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateSystemStatsDisplay(stats) {
|
updateSystemStatsDisplay(stats) {
|
||||||
const cpuEl = document.getElementById('statCpu');
|
const cpuEl = this.$('statCpu');
|
||||||
const cpuBar = document.getElementById('statCpuBar');
|
const cpuBar = this.$('statCpuBar');
|
||||||
const memEl = document.getElementById('statMem');
|
const memEl = this.$('statMem');
|
||||||
const memBar = document.getElementById('statMemBar');
|
const memBar = this.$('statMemBar');
|
||||||
|
|
||||||
if (cpuEl && cpuBar) {
|
if (cpuEl && cpuBar) {
|
||||||
cpuEl.textContent = `${stats.cpu}%`;
|
cpuEl.textContent = `${stats.cpu}%`;
|
||||||
@@ -1896,11 +1966,19 @@ class ClaudemanApp {
|
|||||||
|
|
||||||
// ========== Utility ==========
|
// ========== Utility ==========
|
||||||
|
|
||||||
|
// Pre-compiled HTML escape map for performance (avoids DOM element creation)
|
||||||
|
static _htmlEscapeMap = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
static _htmlEscapePattern = /[&<>"']/g;
|
||||||
|
|
||||||
escapeHtml(text) {
|
escapeHtml(text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
const div = document.createElement('div');
|
return text.replace(ClaudemanApp._htmlEscapePattern, char => ClaudemanApp._htmlEscapeMap[char]);
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -1160,9 +1160,20 @@ export class WebServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optimized: send pre-formatted SSE message to a client
|
||||||
|
private sendSSEPreformatted(reply: FastifyReply, message: string): void {
|
||||||
|
try {
|
||||||
|
reply.raw.write(message);
|
||||||
|
} catch {
|
||||||
|
this.sseClients.delete(reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private broadcast(event: string, data: unknown): void {
|
private broadcast(event: string, data: unknown): void {
|
||||||
|
// Performance optimization: serialize JSON once for all clients
|
||||||
|
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
||||||
for (const client of this.sseClients) {
|
for (const client of this.sseClients) {
|
||||||
this.sendSSE(client, event, data);
|
this.sendSSEPreformatted(client, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user