mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
chore: bump version to 0.1503
This commit is contained in:
@@ -25,7 +25,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.1502 (must match `package.json` for npm publish)
|
**Version**: 0.1503 (must match `package.json` for npm publish)
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"name": "claudeman",
|
||||||
"version": "0.1502",
|
"version": "0.1503",
|
||||||
"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",
|
||||||
|
|||||||
+13
-2
@@ -754,14 +754,25 @@ export class Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toDetailedState() {
|
toDetailedState() {
|
||||||
|
return {
|
||||||
|
...this.toLightDetailedState(),
|
||||||
|
textOutput: this._textOutput.value,
|
||||||
|
terminalBuffer: this._terminalBuffer.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight detailed state that excludes heavy buffers (textOutput, terminalBuffer).
|
||||||
|
* Use for SSE session:updated broadcasts where buffers aren't needed.
|
||||||
|
* Full buffers are fetched on-demand via /api/sessions/:id/terminal.
|
||||||
|
*/
|
||||||
|
toLightDetailedState() {
|
||||||
return {
|
return {
|
||||||
...this.toState(),
|
...this.toState(),
|
||||||
name: this._name,
|
name: this._name,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
claudeSessionId: this._claudeSessionId,
|
claudeSessionId: this._claudeSessionId,
|
||||||
totalCost: this._totalCost,
|
totalCost: this._totalCost,
|
||||||
textOutput: this._textOutput.value,
|
|
||||||
terminalBuffer: this._terminalBuffer.value,
|
|
||||||
messageCount: this._messages.length,
|
messageCount: this._messages.length,
|
||||||
isWorking: this._isWorking,
|
isWorking: this._isWorking,
|
||||||
lastPromptTime: this._lastPromptTime,
|
lastPromptTime: this._lastPromptTime,
|
||||||
|
|||||||
+162
-66
@@ -1171,6 +1171,7 @@ class ClaudemanApp {
|
|||||||
this.teammatePanesByName = new Map(); // Map<name, { paneTarget, sessionId, color }>
|
this.teammatePanesByName = new Map(); // Map<name, { paneTarget, sessionId, color }>
|
||||||
this.teammateTerminals = new Map(); // Map<agentId, { terminal, fitAddon, paneTarget, sessionId, resizeObserver }>
|
this.teammateTerminals = new Map(); // Map<agentId, { terminal, fitAddon, paneTarget, sessionId, resizeObserver }>
|
||||||
|
|
||||||
|
this.terminalBufferCache = new Map(); // Map<sessionId, string> — client-side cache for instant tab re-visits (max 20)
|
||||||
this.ralphStatePanelCollapsed = true; // Default to collapsed
|
this.ralphStatePanelCollapsed = true; // Default to collapsed
|
||||||
this.ralphClosedSessions = new Set(); // Sessions where user explicitly closed Ralph panel
|
this.ralphClosedSessions = new Set(); // Sessions where user explicitly closed Ralph panel
|
||||||
|
|
||||||
@@ -1549,6 +1550,7 @@ class ClaudemanApp {
|
|||||||
// PTY/Ink handles all character echoing to avoid desync ("typing visible below" bug).
|
// PTY/Ink handles all character echoing to avoid desync ("typing visible below" bug).
|
||||||
this._pendingInput = '';
|
this._pendingInput = '';
|
||||||
this._inputFlushTimeout = null;
|
this._inputFlushTimeout = null;
|
||||||
|
this._lastKeystrokeTime = 0;
|
||||||
|
|
||||||
const flushInput = () => {
|
const flushInput = () => {
|
||||||
this._inputFlushTimeout = null;
|
this._inputFlushTimeout = null;
|
||||||
@@ -1580,10 +1582,23 @@ class ClaudemanApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular chars — flush on next microtask to coalesce rapid keystrokes
|
// Regular chars — flush immediately if typed after a gap (>50ms),
|
||||||
// (e.g. paste) while keeping latency near-zero for single keystrokes.
|
// otherwise batch via microtask to coalesce rapid keystrokes (paste).
|
||||||
if (!this._inputFlushTimeout) {
|
const now = performance.now();
|
||||||
this._inputFlushTimeout = setTimeout(flushInput, 0);
|
if (now - this._lastKeystrokeTime > 50) {
|
||||||
|
// Single char after a gap — send immediately, no setTimeout latency
|
||||||
|
if (this._inputFlushTimeout) {
|
||||||
|
clearTimeout(this._inputFlushTimeout);
|
||||||
|
this._inputFlushTimeout = null;
|
||||||
|
}
|
||||||
|
this._lastKeystrokeTime = now;
|
||||||
|
flushInput();
|
||||||
|
} else {
|
||||||
|
// Rapid sequence (paste or fast typing) — coalesce via microtask
|
||||||
|
this._lastKeystrokeTime = now;
|
||||||
|
if (!this._inputFlushTimeout) {
|
||||||
|
this._inputFlushTimeout = setTimeout(flushInput, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2973,22 +2988,31 @@ class ClaudemanApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chain sends sequentially to preserve keystroke ordering
|
// Chain on dispatch only — wait for the previous request to be sent before
|
||||||
this._inputSendChain = this._inputSendChain.then(async () => {
|
// dispatching the next one (preserves keystroke ordering), but don't wait
|
||||||
try {
|
// for the server's response. The server handles writeViaScreen as
|
||||||
const resp = await fetch(`/api/sessions/${sessionId}/input`, {
|
// fire-and-forget anyway, so the HTTP response carries no useful data
|
||||||
method: 'POST',
|
// beyond success/failure for retry purposes.
|
||||||
headers: { 'Content-Type': 'application/json' },
|
this._inputSendChain = this._inputSendChain.then(() => {
|
||||||
body: JSON.stringify({ input })
|
const fetchPromise = fetch(`/api/sessions/${sessionId}/input`, {
|
||||||
});
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ input }),
|
||||||
|
keepalive: input.length < 65536,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle response asynchronously — don't block next keystroke on response
|
||||||
|
fetchPromise.then(resp => {
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
this._enqueueInput(sessionId, input);
|
this._enqueueInput(sessionId, input);
|
||||||
} else {
|
} else {
|
||||||
this.clearPendingHooks(sessionId);
|
this.clearPendingHooks(sessionId);
|
||||||
}
|
}
|
||||||
} catch {
|
}).catch(() => {
|
||||||
this._enqueueInput(sessionId, input);
|
this._enqueueInput(sessionId, input);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
// Return immediately after fetch is dispatched (don't await response)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3097,6 +3121,7 @@ class ClaudemanApp {
|
|||||||
this.sessions.clear();
|
this.sessions.clear();
|
||||||
this.ralphStates.clear();
|
this.ralphStates.clear();
|
||||||
this.terminalBuffers.clear();
|
this.terminalBuffers.clear();
|
||||||
|
this.terminalBufferCache.clear();
|
||||||
this.projectInsights.clear();
|
this.projectInsights.clear();
|
||||||
// Clear all idle timers to prevent stale timers from firing
|
// Clear all idle timers to prevent stale timers from firing
|
||||||
for (const timer of this.idleTimers.values()) {
|
for (const timer of this.idleTimers.values()) {
|
||||||
@@ -3284,6 +3309,20 @@ class ClaudemanApp {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Toggle .active class on tabs immediately (no debounce). Used by selectSession(). */
|
||||||
|
_updateActiveTabImmediate(sessionId) {
|
||||||
|
const container = this.$('sessionTabs');
|
||||||
|
if (!container) return;
|
||||||
|
const tabs = container.querySelectorAll('.session-tab[data-id]');
|
||||||
|
for (const tab of tabs) {
|
||||||
|
if (tab.dataset.id === sessionId) {
|
||||||
|
tab.classList.add('active');
|
||||||
|
} else {
|
||||||
|
tab.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_renderSessionTabsImmediate() {
|
_renderSessionTabsImmediate() {
|
||||||
const container = this.$('sessionTabs');
|
const container = this.$('sessionTabs');
|
||||||
const existingTabs = container.querySelectorAll('.session-tab[data-id]');
|
const existingTabs = container.querySelectorAll('.session-tab[data-id]');
|
||||||
@@ -3831,6 +3870,8 @@ class ClaudemanApp {
|
|||||||
this.hideWelcome();
|
this.hideWelcome();
|
||||||
// Clear idle hooks on view, but keep action hooks until user interacts
|
// Clear idle hooks on view, but keep action hooks until user interacts
|
||||||
this.clearPendingHooks(sessionId, 'idle_prompt');
|
this.clearPendingHooks(sessionId, 'idle_prompt');
|
||||||
|
// Instant active-class toggle (no 100ms debounce), then schedule full render for badges/status
|
||||||
|
this._updateActiveTabImmediate(sessionId);
|
||||||
this.renderSessionTabs();
|
this.renderSessionTabs();
|
||||||
|
|
||||||
// Glow the newly-active tab
|
// Glow the newly-active tab
|
||||||
@@ -3857,9 +3898,19 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load terminal buffer for this session
|
// Load terminal buffer for this session
|
||||||
// Use tail mode for faster initial load (256KB is enough for recent visible content)
|
// Show cached content instantly while fetching fresh data in background.
|
||||||
|
// Use tail mode for faster initial load (256KB is enough for recent visible content).
|
||||||
// Fetch runs in parallel with fade-out; we wait for both before clearing terminal.
|
// Fetch runs in parallel with fade-out; we wait for both before clearing terminal.
|
||||||
try {
|
try {
|
||||||
|
// Instant cache restore — show previous buffer immediately while network fetch runs
|
||||||
|
const cachedBuffer = this.terminalBufferCache.get(sessionId);
|
||||||
|
if (cachedBuffer) {
|
||||||
|
this.terminal.clear();
|
||||||
|
this.terminal.reset();
|
||||||
|
this.terminal.write(cachedBuffer);
|
||||||
|
this.terminal.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
const tailSize = 256 * 1024;
|
const tailSize = 256 * 1024;
|
||||||
const [res] = await Promise.all([
|
const [res] = await Promise.all([
|
||||||
fetch(`/api/sessions/${sessionId}/terminal?tail=${tailSize}`),
|
fetch(`/api/sessions/${sessionId}/terminal?tail=${tailSize}`),
|
||||||
@@ -3880,61 +3931,76 @@ class ClaudemanApp {
|
|||||||
if (selectGen !== this._selectGeneration) return; // stale — skip post-write UI updates
|
if (selectGen !== this._selectGeneration) return; // stale — skip post-write UI updates
|
||||||
// Ensure terminal is scrolled to bottom after buffer load
|
// Ensure terminal is scrolled to bottom after buffer load
|
||||||
this.terminal.scrollToBottom();
|
this.terminal.scrollToBottom();
|
||||||
|
|
||||||
|
// Update cache (cap at 20 entries)
|
||||||
|
this.terminalBufferCache.set(sessionId, data.terminalBuffer);
|
||||||
|
if (this.terminalBufferCache.size > 20) {
|
||||||
|
// Evict oldest entry (first key in Map iteration order)
|
||||||
|
const oldest = this.terminalBufferCache.keys().next().value;
|
||||||
|
this.terminalBufferCache.delete(oldest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send resize and Ctrl+L to trigger Claude to redraw at correct size
|
// Send resize and Ctrl+L to trigger Claude to redraw at correct size
|
||||||
await this.sendResize(sessionId);
|
await this.sendResize(sessionId);
|
||||||
|
|
||||||
// Update respawn banner
|
// Defer secondary panel updates so they don't block the main thread
|
||||||
if (this.respawnStatus[sessionId]) {
|
// after terminal content is already visible.
|
||||||
this.showRespawnBanner();
|
const idleCb = typeof requestIdleCallback === 'function' ? requestIdleCallback : (cb) => setTimeout(cb, 16);
|
||||||
this.updateRespawnBanner(this.respawnStatus[sessionId].state);
|
idleCb(() => {
|
||||||
document.getElementById('respawnCycleCount').textContent = this.respawnStatus[sessionId].cycleCount || 0;
|
// Guard against stale generation — user may have switched tabs again
|
||||||
// Update countdown timers and action log for this session
|
if (selectGen !== this._selectGeneration) return;
|
||||||
this.updateCountdownTimerDisplay();
|
|
||||||
this.updateActionLogDisplay();
|
// Update respawn banner
|
||||||
if (Object.keys(this.respawnCountdownTimers[sessionId] || {}).length > 0) {
|
if (this.respawnStatus[sessionId]) {
|
||||||
this.startCountdownInterval();
|
this.showRespawnBanner();
|
||||||
|
this.updateRespawnBanner(this.respawnStatus[sessionId].state);
|
||||||
|
document.getElementById('respawnCycleCount').textContent = this.respawnStatus[sessionId].cycleCount || 0;
|
||||||
|
this.updateCountdownTimerDisplay();
|
||||||
|
this.updateActionLogDisplay();
|
||||||
|
if (Object.keys(this.respawnCountdownTimers[sessionId] || {}).length > 0) {
|
||||||
|
this.startCountdownInterval();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.hideRespawnBanner();
|
||||||
|
this.stopCountdownInterval();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.hideRespawnBanner();
|
|
||||||
this.stopCountdownInterval();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update task panel if open
|
// Update task panel if open
|
||||||
const taskPanel = document.getElementById('taskPanel');
|
const taskPanel = document.getElementById('taskPanel');
|
||||||
if (taskPanel && taskPanel.classList.contains('open')) {
|
if (taskPanel && taskPanel.classList.contains('open')) {
|
||||||
this.renderTaskPanel();
|
this.renderTaskPanel();
|
||||||
}
|
|
||||||
|
|
||||||
// Update ralph state panel for this session
|
|
||||||
const session = this.sessions.get(sessionId);
|
|
||||||
if (session && (session.ralphLoop || session.ralphTodos)) {
|
|
||||||
this.updateRalphState(sessionId, {
|
|
||||||
loop: session.ralphLoop,
|
|
||||||
todos: session.ralphTodos
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.renderRalphStatePanel();
|
|
||||||
|
|
||||||
// Update CLI info bar (mobile - shows Claude version/model)
|
|
||||||
this.updateCliInfoDisplay();
|
|
||||||
|
|
||||||
// Update project insights panel for this session
|
|
||||||
this.renderProjectInsightsPanel();
|
|
||||||
|
|
||||||
// Update subagent window visibility for active session
|
|
||||||
this.updateSubagentWindowVisibility();
|
|
||||||
|
|
||||||
// Load file browser if enabled
|
|
||||||
const settings = this.loadAppSettingsFromStorage();
|
|
||||||
if (settings.showFileBrowser) {
|
|
||||||
const fileBrowserPanel = this.$('fileBrowserPanel');
|
|
||||||
if (fileBrowserPanel) {
|
|
||||||
fileBrowserPanel.classList.add('visible');
|
|
||||||
this.loadFileBrowser(sessionId);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Update ralph state panel for this session
|
||||||
|
const curSession = this.sessions.get(sessionId);
|
||||||
|
if (curSession && (curSession.ralphLoop || curSession.ralphTodos)) {
|
||||||
|
this.updateRalphState(sessionId, {
|
||||||
|
loop: curSession.ralphLoop,
|
||||||
|
todos: curSession.ralphTodos
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.renderRalphStatePanel();
|
||||||
|
|
||||||
|
// Update CLI info bar (mobile - shows Claude version/model)
|
||||||
|
this.updateCliInfoDisplay();
|
||||||
|
|
||||||
|
// Update project insights panel for this session
|
||||||
|
this.renderProjectInsightsPanel();
|
||||||
|
|
||||||
|
// Update subagent window visibility for active session
|
||||||
|
this.updateSubagentWindowVisibility();
|
||||||
|
|
||||||
|
// Load file browser if enabled
|
||||||
|
const settings = this.loadAppSettingsFromStorage();
|
||||||
|
if (settings.showFileBrowser) {
|
||||||
|
const fileBrowserPanel = this.$('fileBrowserPanel');
|
||||||
|
if (fileBrowserPanel) {
|
||||||
|
fileBrowserPanel.classList.add('visible');
|
||||||
|
this.loadFileBrowser(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.terminal.focus();
|
this.terminal.focus();
|
||||||
this.terminal.scrollToBottom();
|
this.terminal.scrollToBottom();
|
||||||
@@ -3964,6 +4030,7 @@ class ClaudemanApp {
|
|||||||
this.saveSessionOrder();
|
this.saveSessionOrder();
|
||||||
}
|
}
|
||||||
this.terminalBuffers.delete(sessionId);
|
this.terminalBuffers.delete(sessionId);
|
||||||
|
this.terminalBufferCache.delete(sessionId);
|
||||||
this.ralphStates.delete(sessionId);
|
this.ralphStates.delete(sessionId);
|
||||||
this.clearCountdownTimers(sessionId);
|
this.clearCountdownTimers(sessionId);
|
||||||
|
|
||||||
@@ -7548,10 +7615,13 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateRespawnTokens(tokens) {
|
updateRespawnTokens(tokens) {
|
||||||
const tokensEl = this.$('respawnTokens');
|
// Skip if tokens haven't changed (avoid unnecessary DOM writes)
|
||||||
// Support both old format (number) and new format (object with input/output/total)
|
|
||||||
const isObject = tokens && typeof tokens === 'object';
|
const isObject = tokens && typeof tokens === 'object';
|
||||||
const total = isObject ? tokens.total : tokens;
|
const total = isObject ? tokens.total : tokens;
|
||||||
|
if (total === this._lastRespawnTokenTotal) return;
|
||||||
|
this._lastRespawnTokenTotal = total;
|
||||||
|
|
||||||
|
const tokensEl = this.$('respawnTokens');
|
||||||
const input = isObject ? (tokens.input || 0) : Math.round(total * 0.6);
|
const input = isObject ? (tokens.input || 0) : Math.round(total * 0.6);
|
||||||
const output = isObject ? (tokens.output || 0) : Math.round(total * 0.4);
|
const output = isObject ? (tokens.output || 0) : Math.round(total * 0.4);
|
||||||
|
|
||||||
@@ -7801,6 +7871,7 @@ class ClaudemanApp {
|
|||||||
await fetch('/api/sessions', { method: 'DELETE' });
|
await fetch('/api/sessions', { method: 'DELETE' });
|
||||||
this.sessions.clear();
|
this.sessions.clear();
|
||||||
this.terminalBuffers.clear();
|
this.terminalBuffers.clear();
|
||||||
|
this.terminalBufferCache.clear();
|
||||||
this.activeSessionId = null;
|
this.activeSessionId = null;
|
||||||
try { localStorage.removeItem('claudeman-active-session'); } catch {}
|
try { localStorage.removeItem('claudeman-active-session'); } catch {}
|
||||||
this.respawnStatus = {};
|
this.respawnStatus = {};
|
||||||
@@ -8029,6 +8100,18 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateTokens() {
|
updateTokens() {
|
||||||
|
// Debounce at 200ms — token display is non-critical and shouldn't
|
||||||
|
// compete with input handling on the main thread
|
||||||
|
if (this._updateTokensTimeout) {
|
||||||
|
clearTimeout(this._updateTokensTimeout);
|
||||||
|
}
|
||||||
|
this._updateTokensTimeout = setTimeout(() => {
|
||||||
|
this._updateTokensTimeout = null;
|
||||||
|
this._updateTokensImmediate();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateTokensImmediate() {
|
||||||
// Use global stats if available (includes deleted sessions)
|
// Use global stats if available (includes deleted sessions)
|
||||||
let totalInput = 0;
|
let totalInput = 0;
|
||||||
let totalOutput = 0;
|
let totalOutput = 0;
|
||||||
@@ -9363,20 +9446,27 @@ class ClaudemanApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadAppSettingsFromStorage() {
|
loadAppSettingsFromStorage() {
|
||||||
|
// Return cached settings if available (avoids synchronous localStorage + JSON.parse
|
||||||
|
// on every SSE event — critical for input responsiveness)
|
||||||
|
if (this._cachedAppSettings) return this._cachedAppSettings;
|
||||||
try {
|
try {
|
||||||
const key = this.getSettingsStorageKey();
|
const key = this.getSettingsStorageKey();
|
||||||
const saved = localStorage.getItem(key);
|
const saved = localStorage.getItem(key);
|
||||||
if (saved) {
|
if (saved) {
|
||||||
return JSON.parse(saved);
|
this._cachedAppSettings = JSON.parse(saved);
|
||||||
|
return this._cachedAppSettings;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load app settings:', err);
|
console.error('Failed to load app settings:', err);
|
||||||
}
|
}
|
||||||
// Return device-specific defaults
|
// Return device-specific defaults
|
||||||
return this.getDefaultSettings();
|
this._cachedAppSettings = this.getDefaultSettings();
|
||||||
|
return this._cachedAppSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveAppSettingsToStorage(settings) {
|
saveAppSettingsToStorage(settings) {
|
||||||
|
// Invalidate cache on save
|
||||||
|
this._cachedAppSettings = settings;
|
||||||
try {
|
try {
|
||||||
const key = this.getSettingsStorageKey();
|
const key = this.getSettingsStorageKey();
|
||||||
localStorage.setItem(key, JSON.stringify(settings));
|
localStorage.setItem(key, JSON.stringify(settings));
|
||||||
@@ -11740,6 +11830,12 @@ class ClaudemanApp {
|
|||||||
|
|
||||||
const newName = this.getSessionName(session);
|
const newName = this.getSessionName(session);
|
||||||
|
|
||||||
|
// Skip iteration if name hasn't changed (avoids O(n) loop on every session:updated)
|
||||||
|
const cachedName = this._parentNameCache?.get(sessionId);
|
||||||
|
if (cachedName === newName) return;
|
||||||
|
if (!this._parentNameCache) this._parentNameCache = new Map();
|
||||||
|
this._parentNameCache.set(sessionId, newName);
|
||||||
|
|
||||||
for (const [agentId, storedSessionId] of this.subagentParentMap) {
|
for (const [agentId, storedSessionId] of this.subagentParentMap) {
|
||||||
if (storedSessionId === sessionId) {
|
if (storedSessionId === sessionId) {
|
||||||
const agent = this.subagents.get(agentId);
|
const agent = this.subagents.get(agentId);
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ body {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100; /* Above main content so dropdowns work */
|
z-index: 100; /* Above main content so dropdowns work */
|
||||||
|
/* Performance: isolate header from terminal reflow — tab badge updates
|
||||||
|
and token counter changes shouldn't trigger terminal layout recalculation */
|
||||||
|
contain: layout style paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-brand {
|
.header-brand {
|
||||||
@@ -204,7 +207,7 @@ body {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
transition: all 0.15s;
|
transition: background 0.05s, border-color 0.05s, color 0.05s, box-shadow 0.05s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tab:hover {
|
.session-tab:hover {
|
||||||
@@ -228,7 +231,7 @@ body {
|
|||||||
|
|
||||||
/* Tab switch feedback: bright green glow on the newly-active tab */
|
/* Tab switch feedback: bright green glow on the newly-active tab */
|
||||||
.session-tab.tab-glow {
|
.session-tab.tab-glow {
|
||||||
animation: tab-glow 1s ease-out forwards;
|
animation: tab-glow 0.35s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes tab-glow {
|
@keyframes tab-glow {
|
||||||
@@ -288,7 +291,7 @@ body {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
/* Performance: use transform for GPU acceleration */
|
/* Performance: use transform for GPU acceleration */
|
||||||
transition: opacity 0.15s ease-out, width 0.15s ease-out, padding 0.15s ease-out;
|
transition: opacity 0.05s ease-out, width 0.05s ease-out, padding 0.05s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tab:hover .tab-close {
|
.session-tab:hover .tab-close {
|
||||||
@@ -1674,6 +1677,10 @@ body {
|
|||||||
Do NOT use contain:strict — size containment breaks xterm.js
|
Do NOT use contain:strict — size containment breaks xterm.js
|
||||||
FitAddon dimension calculations. */
|
FitAddon dimension calculations. */
|
||||||
contain: layout paint;
|
contain: layout paint;
|
||||||
|
/* Performance: promote to own compositor layer for isolation from
|
||||||
|
header/toolbar/panel repaints. Eliminates compositing stalls
|
||||||
|
when surrounding UI elements update during typing. */
|
||||||
|
will-change: contents;
|
||||||
}
|
}
|
||||||
/* Tab switch: green-tinted overlay fades in over old content, fades out to reveal new.
|
/* Tab switch: green-tinted overlay fades in over old content, fades out to reveal new.
|
||||||
Animated via Web Animations API (not CSS keyframes) for reliable GPU compositing.
|
Animated via Web Animations API (not CSS keyframes) for reliable GPU compositing.
|
||||||
@@ -1812,6 +1819,8 @@ body {
|
|||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
/* Performance: isolate from terminal — toolbar updates shouldn't trigger terminal reflow */
|
||||||
|
contain: layout style paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-left,
|
.toolbar-left,
|
||||||
|
|||||||
+4
-10
@@ -4314,7 +4314,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
private getSessionStateWithRespawn(session: Session) {
|
private getSessionStateWithRespawn(session: Session) {
|
||||||
const controller = this.respawnControllers.get(session.id);
|
const controller = this.respawnControllers.get(session.id);
|
||||||
return {
|
return {
|
||||||
...session.toDetailedState(),
|
...session.toLightDetailedState(),
|
||||||
respawnEnabled: controller?.getConfig()?.enabled ?? false,
|
respawnEnabled: controller?.getConfig()?.enabled ?? false,
|
||||||
respawnConfig: controller?.getConfig() ?? null,
|
respawnConfig: controller?.getConfig() ?? null,
|
||||||
respawn: controller?.getStatus() ?? null,
|
respawn: controller?.getStatus() ?? null,
|
||||||
@@ -4327,15 +4327,9 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
|||||||
* on-demand when switching tabs via /api/sessions/:id/buffer
|
* on-demand when switching tabs via /api/sessions/:id/buffer
|
||||||
*/
|
*/
|
||||||
private getLightSessionsState() {
|
private getLightSessionsState() {
|
||||||
return Array.from(this.sessions.values()).map(s => {
|
// getSessionStateWithRespawn already uses toLightDetailedState() which
|
||||||
const state = this.getSessionStateWithRespawn(s);
|
// excludes terminalBuffer and textOutput — no extra stripping needed
|
||||||
return {
|
return Array.from(this.sessions.values()).map(s => this.getSessionStateWithRespawn(s));
|
||||||
...state,
|
|
||||||
// Exclude full buffers - they're fetched on-demand
|
|
||||||
terminalBuffer: '',
|
|
||||||
textOutput: '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up old completed scheduled runs
|
// Clean up old completed scheduled runs
|
||||||
|
|||||||
Reference in New Issue
Block a user