chore: bump version to 0.1580

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-20 15:04:45 +01:00
co-authored by Claude Opus 4.6
parent 3f9632c8c7
commit 819b1771c3
8 changed files with 89 additions and 31 deletions
+33 -3
View File
@@ -33,6 +33,15 @@ const WINDOW_MIN_WIDTH_PX = 200;
const WINDOW_MIN_HEIGHT_PX = 200;
const WINDOW_DEFAULT_WIDTH_PX = 300;
// Scheduler API — prioritize terminal writes over background UI updates.
// scheduler.postTask('background') defers non-critical work (connection lines, panel renders)
// so the main thread stays free for terminal rendering at 60fps.
const _hasScheduler = typeof globalThis.scheduler?.postTask === 'function';
function scheduleBackground(fn) {
if (_hasScheduler) { scheduler.postTask(fn, { priority: 'background' }); }
else { requestAnimationFrame(fn); }
}
// DEC mode 2026 - Synchronized Output
// Wrap terminal writes with these markers to prevent partial-frame flicker.
// Terminal buffers all output between markers and renders atomically.
@@ -1871,6 +1880,16 @@ class ClaudemanApp {
const container = document.getElementById('terminalContainer');
this.terminal.open(container);
// Activate WebGL renderer for up to 900% faster rendering (fallback to canvas on failure)
if (typeof WebglAddon !== 'undefined') {
try {
const webglAddon = new WebglAddon.WebglAddon();
webglAddon.onContextLoss(() => { webglAddon.dispose(); });
this.terminal.loadAddon(webglAddon);
} catch (_e) { /* WebGL2 unavailable — canvas renderer used */ }
}
this._localEchoOverlay = new LocalEchoOverlay(this.terminal);
// On mobile Safari, delay initial fit() to allow layout to settle
@@ -3624,6 +3643,16 @@ class ClaudemanApp {
this.subagentToolResults.delete(data.agentId);
}
}, 5 * 60 * 1000); // 5 minutes
// Prune stale completed agents from main maps after 30 minutes
// Keeps subagents/subagentParentMap from growing unbounded in 24h sessions
setTimeout(() => {
const agent = this.subagents.get(data.agentId);
if (agent?.status === 'completed' && !this.subagentWindows.has(data.agentId)) {
this.subagents.delete(data.agentId);
this.subagentParentMap.delete(data.agentId);
}
}, 30 * 60 * 1000); // 30 minutes
});
// ========== Image Detection Events (Screenshots & Generated Images) ==========
@@ -12399,7 +12428,7 @@ class ClaudemanApp {
clearTimeout(this._subagentPanelRenderTimeout);
}
this._subagentPanelRenderTimeout = setTimeout(() => {
this._renderSubagentPanelImmediate();
scheduleBackground(() => this._renderSubagentPanelImmediate());
}, 150);
}
@@ -12902,10 +12931,11 @@ class ClaudemanApp {
// This map stores agentId -> sessionId, where sessionId is the tab's data-id.
updateConnectionLines() {
// Coalesce multiple calls within the same frame into a single rAF
// Coalesce multiple calls — uses background scheduler priority to avoid
// competing with terminal writes for main thread time
if (!this._connectionLinesScheduled) {
this._connectionLinesScheduled = true;
requestAnimationFrame(() => {
scheduleBackground(() => {
this._connectionLinesScheduled = false;
this._updateConnectionLinesImmediate();
});
+1
View File
@@ -17,6 +17,7 @@
'defer' preserves execution order (xterm loads before fit addon). -->
<script defer src="vendor/xterm.min.js"></script>
<script defer src="vendor/xterm-addon-fit.min.js"></script>
<script defer src="vendor/xterm-addon-webgl.min.js"></script>
<!-- Synchronous mobile detection — runs before first paint to prevent panel flash -->
<script>if(window.innerWidth<768||(('ontouchstart' in window||navigator.maxTouchPoints>0)&&window.innerWidth<1024))document.documentElement.classList.add('mobile-init');</script>
<!-- Inline critical CSS for instant skeleton paint (before styles.css loads) -->
+8
View File
@@ -2990,6 +2990,8 @@ body {
border-radius: 6px;
border-left: 2px solid var(--border);
transition: background 0.15s;
content-visibility: auto;
contain-intrinsic-size: auto 40px;
}
.timeline-event:hover {
@@ -3874,6 +3876,8 @@ kbd {
cursor: pointer;
transition: background 0.15s;
animation: notif-slide-in 0.2s ease;
content-visibility: auto;
contain-intrinsic-size: auto 60px;
}
.notif-item:hover {
@@ -4631,6 +4635,8 @@ kbd {
margin-bottom: 0.4rem;
cursor: pointer;
transition: background 0.15s;
content-visibility: auto;
contain-intrinsic-size: auto 48px;
}
.subagent-item:hover {
@@ -5731,6 +5737,8 @@ kbd {
.log-viewer-window-body .log-line {
padding: 0.1rem 0;
content-visibility: auto;
contain-intrinsic-size: auto 20px;
}
.log-viewer-window-body .log-error {
+26 -23
View File
@@ -632,11 +632,13 @@ export class WebServer extends EventEmitter {
});
// Serve static files — versioned assets (?v=X) are immutable, cache aggressively
// preCompressed: serve pre-built .br/.gz files (from build step) to avoid per-request CPU compression
await this.app.register(fastifyStatic, {
root: join(__dirname, 'public'),
prefix: '/',
maxAge: '1y',
immutable: true,
preCompressed: true,
});
// SSE endpoint for real-time updates
@@ -1756,32 +1758,15 @@ export class WebServer extends EventEmitter {
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Session not found');
}
// Clean the buffer: remove junk before actual Claude content
let cleanBuffer = session.terminalBuffer;
// Find where Claude banner starts (has color codes before "Claude")
const claudeMatch = cleanBuffer.match(CLAUDE_BANNER_PATTERN);
if (claudeMatch && claudeMatch.index !== undefined && claudeMatch.index > 0) {
// Find the start of that line
let lineStart = claudeMatch.index;
while (lineStart > 0 && cleanBuffer[lineStart - 1] !== '\n') {
lineStart--;
}
cleanBuffer = cleanBuffer.slice(lineStart);
}
// Remove Ctrl+L and leading whitespace
cleanBuffer = cleanBuffer
.replace(CTRL_L_PATTERN, '')
.replace(LEADING_WHITESPACE_PATTERN, '');
// Optionally truncate to last N bytes for faster initial load
const tailBytes = query.tail ? parseInt(query.tail, 10) : 0;
const fullSize = cleanBuffer.length;
const fullSize = session.terminalBufferLength;
let truncated = false;
let cleanBuffer: string;
if (tailBytes > 0 && cleanBuffer.length > tailBytes) {
cleanBuffer = cleanBuffer.slice(-tailBytes);
if (tailBytes > 0 && fullSize > tailBytes) {
// Fast path: tail from the end, skip expensive banner search on full 2MB buffer.
// Banner is near the top and gets discarded by tail anyway.
cleanBuffer = session.terminalBuffer.slice(-tailBytes);
truncated = true;
// Avoid starting mid-ANSI-escape: find first newline within the first 4KB
// and start from there. This prevents xterm.js from parsing a partial escape
@@ -1790,8 +1775,26 @@ export class WebServer extends EventEmitter {
if (firstNewline > 0 && firstNewline < 4096) {
cleanBuffer = cleanBuffer.slice(firstNewline + 1);
}
} else {
// Full buffer: clean junk before actual Claude content
cleanBuffer = session.terminalBuffer;
// Find where Claude banner starts (has color codes before "Claude")
const claudeMatch = cleanBuffer.match(CLAUDE_BANNER_PATTERN);
if (claudeMatch && claudeMatch.index !== undefined && claudeMatch.index > 0) {
let lineStart = claudeMatch.index;
while (lineStart > 0 && cleanBuffer[lineStart - 1] !== '\n') {
lineStart--;
}
cleanBuffer = cleanBuffer.slice(lineStart);
}
}
// Remove Ctrl+L and leading whitespace (cheap on tailed subset)
cleanBuffer = cleanBuffer
.replace(CTRL_L_PATTERN, '')
.replace(LEADING_WHITESPACE_PATTERN, '');
return {
terminalBuffer: cleanBuffer,
status: session.status,