mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +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`)
|
||||
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
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
+13
-2
@@ -754,14 +754,25 @@ export class Session extends EventEmitter {
|
||||
}
|
||||
|
||||
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 {
|
||||
...this.toState(),
|
||||
name: this._name,
|
||||
mode: this.mode,
|
||||
claudeSessionId: this._claudeSessionId,
|
||||
totalCost: this._totalCost,
|
||||
textOutput: this._textOutput.value,
|
||||
terminalBuffer: this._terminalBuffer.value,
|
||||
messageCount: this._messages.length,
|
||||
isWorking: this._isWorking,
|
||||
lastPromptTime: this._lastPromptTime,
|
||||
|
||||
+162
-66
@@ -1171,6 +1171,7 @@ class ClaudemanApp {
|
||||
this.teammatePanesByName = new Map(); // Map<name, { paneTarget, sessionId, color }>
|
||||
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.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).
|
||||
this._pendingInput = '';
|
||||
this._inputFlushTimeout = null;
|
||||
this._lastKeystrokeTime = 0;
|
||||
|
||||
const flushInput = () => {
|
||||
this._inputFlushTimeout = null;
|
||||
@@ -1580,10 +1582,23 @@ class ClaudemanApp {
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular chars — flush on next microtask to coalesce rapid keystrokes
|
||||
// (e.g. paste) while keeping latency near-zero for single keystrokes.
|
||||
if (!this._inputFlushTimeout) {
|
||||
this._inputFlushTimeout = setTimeout(flushInput, 0);
|
||||
// Regular chars — flush immediately if typed after a gap (>50ms),
|
||||
// otherwise batch via microtask to coalesce rapid keystrokes (paste).
|
||||
const now = performance.now();
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 })
|
||||
});
|
||||
// Chain on dispatch only — wait for the previous request to be sent before
|
||||
// dispatching the next one (preserves keystroke ordering), but don't wait
|
||||
// for the server's response. The server handles writeViaScreen as
|
||||
// fire-and-forget anyway, so the HTTP response carries no useful data
|
||||
// beyond success/failure for retry purposes.
|
||||
this._inputSendChain = this._inputSendChain.then(() => {
|
||||
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) {
|
||||
this._enqueueInput(sessionId, input);
|
||||
} else {
|
||||
this.clearPendingHooks(sessionId);
|
||||
}
|
||||
} catch {
|
||||
}).catch(() => {
|
||||
this._enqueueInput(sessionId, input);
|
||||
}
|
||||
});
|
||||
|
||||
// Return immediately after fetch is dispatched (don't await response)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3097,6 +3121,7 @@ class ClaudemanApp {
|
||||
this.sessions.clear();
|
||||
this.ralphStates.clear();
|
||||
this.terminalBuffers.clear();
|
||||
this.terminalBufferCache.clear();
|
||||
this.projectInsights.clear();
|
||||
// Clear all idle timers to prevent stale timers from firing
|
||||
for (const timer of this.idleTimers.values()) {
|
||||
@@ -3284,6 +3309,20 @@ class ClaudemanApp {
|
||||
}, 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() {
|
||||
const container = this.$('sessionTabs');
|
||||
const existingTabs = container.querySelectorAll('.session-tab[data-id]');
|
||||
@@ -3831,6 +3870,8 @@ class ClaudemanApp {
|
||||
this.hideWelcome();
|
||||
// Clear idle hooks on view, but keep action hooks until user interacts
|
||||
this.clearPendingHooks(sessionId, 'idle_prompt');
|
||||
// Instant active-class toggle (no 100ms debounce), then schedule full render for badges/status
|
||||
this._updateActiveTabImmediate(sessionId);
|
||||
this.renderSessionTabs();
|
||||
|
||||
// Glow the newly-active tab
|
||||
@@ -3857,9 +3898,19 @@ class ClaudemanApp {
|
||||
}
|
||||
|
||||
// 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.
|
||||
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 [res] = await Promise.all([
|
||||
fetch(`/api/sessions/${sessionId}/terminal?tail=${tailSize}`),
|
||||
@@ -3880,61 +3931,76 @@ class ClaudemanApp {
|
||||
if (selectGen !== this._selectGeneration) return; // stale — skip post-write UI updates
|
||||
// Ensure terminal is scrolled to bottom after buffer load
|
||||
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
|
||||
await this.sendResize(sessionId);
|
||||
|
||||
// Update respawn banner
|
||||
if (this.respawnStatus[sessionId]) {
|
||||
this.showRespawnBanner();
|
||||
this.updateRespawnBanner(this.respawnStatus[sessionId].state);
|
||||
document.getElementById('respawnCycleCount').textContent = this.respawnStatus[sessionId].cycleCount || 0;
|
||||
// Update countdown timers and action log for this session
|
||||
this.updateCountdownTimerDisplay();
|
||||
this.updateActionLogDisplay();
|
||||
if (Object.keys(this.respawnCountdownTimers[sessionId] || {}).length > 0) {
|
||||
this.startCountdownInterval();
|
||||
// Defer secondary panel updates so they don't block the main thread
|
||||
// after terminal content is already visible.
|
||||
const idleCb = typeof requestIdleCallback === 'function' ? requestIdleCallback : (cb) => setTimeout(cb, 16);
|
||||
idleCb(() => {
|
||||
// Guard against stale generation — user may have switched tabs again
|
||||
if (selectGen !== this._selectGeneration) return;
|
||||
|
||||
// Update respawn banner
|
||||
if (this.respawnStatus[sessionId]) {
|
||||
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
|
||||
const taskPanel = document.getElementById('taskPanel');
|
||||
if (taskPanel && taskPanel.classList.contains('open')) {
|
||||
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 task panel if open
|
||||
const taskPanel = document.getElementById('taskPanel');
|
||||
if (taskPanel && taskPanel.classList.contains('open')) {
|
||||
this.renderTaskPanel();
|
||||
}
|
||||
}
|
||||
|
||||
// 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.scrollToBottom();
|
||||
@@ -3964,6 +4030,7 @@ class ClaudemanApp {
|
||||
this.saveSessionOrder();
|
||||
}
|
||||
this.terminalBuffers.delete(sessionId);
|
||||
this.terminalBufferCache.delete(sessionId);
|
||||
this.ralphStates.delete(sessionId);
|
||||
this.clearCountdownTimers(sessionId);
|
||||
|
||||
@@ -7548,10 +7615,13 @@ class ClaudemanApp {
|
||||
}
|
||||
|
||||
updateRespawnTokens(tokens) {
|
||||
const tokensEl = this.$('respawnTokens');
|
||||
// Support both old format (number) and new format (object with input/output/total)
|
||||
// Skip if tokens haven't changed (avoid unnecessary DOM writes)
|
||||
const isObject = tokens && typeof tokens === 'object';
|
||||
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 output = isObject ? (tokens.output || 0) : Math.round(total * 0.4);
|
||||
|
||||
@@ -7801,6 +7871,7 @@ class ClaudemanApp {
|
||||
await fetch('/api/sessions', { method: 'DELETE' });
|
||||
this.sessions.clear();
|
||||
this.terminalBuffers.clear();
|
||||
this.terminalBufferCache.clear();
|
||||
this.activeSessionId = null;
|
||||
try { localStorage.removeItem('claudeman-active-session'); } catch {}
|
||||
this.respawnStatus = {};
|
||||
@@ -8029,6 +8100,18 @@ class ClaudemanApp {
|
||||
}
|
||||
|
||||
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)
|
||||
let totalInput = 0;
|
||||
let totalOutput = 0;
|
||||
@@ -9363,20 +9446,27 @@ class ClaudemanApp {
|
||||
}
|
||||
|
||||
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 {
|
||||
const key = this.getSettingsStorageKey();
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
return JSON.parse(saved);
|
||||
this._cachedAppSettings = JSON.parse(saved);
|
||||
return this._cachedAppSettings;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load app settings:', err);
|
||||
}
|
||||
// Return device-specific defaults
|
||||
return this.getDefaultSettings();
|
||||
this._cachedAppSettings = this.getDefaultSettings();
|
||||
return this._cachedAppSettings;
|
||||
}
|
||||
|
||||
saveAppSettingsToStorage(settings) {
|
||||
// Invalidate cache on save
|
||||
this._cachedAppSettings = settings;
|
||||
try {
|
||||
const key = this.getSettingsStorageKey();
|
||||
localStorage.setItem(key, JSON.stringify(settings));
|
||||
@@ -11740,6 +11830,12 @@ class ClaudemanApp {
|
||||
|
||||
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) {
|
||||
if (storedSessionId === sessionId) {
|
||||
const agent = this.subagents.get(agentId);
|
||||
|
||||
@@ -131,6 +131,9 @@ body {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
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 {
|
||||
@@ -204,7 +207,7 @@ body {
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
@@ -228,7 +231,7 @@ body {
|
||||
|
||||
/* Tab switch feedback: bright green glow on the newly-active tab */
|
||||
.session-tab.tab-glow {
|
||||
animation: tab-glow 1s ease-out forwards;
|
||||
animation: tab-glow 0.35s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes tab-glow {
|
||||
@@ -288,7 +291,7 @@ body {
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
/* 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 {
|
||||
@@ -1674,6 +1677,10 @@ body {
|
||||
Do NOT use contain:strict — size containment breaks xterm.js
|
||||
FitAddon dimension calculations. */
|
||||
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.
|
||||
Animated via Web Animations API (not CSS keyframes) for reliable GPU compositing.
|
||||
@@ -1812,6 +1819,8 @@ body {
|
||||
background: var(--bg-card);
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
/* Performance: isolate from terminal — toolbar updates shouldn't trigger terminal reflow */
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
.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) {
|
||||
const controller = this.respawnControllers.get(session.id);
|
||||
return {
|
||||
...session.toDetailedState(),
|
||||
...session.toLightDetailedState(),
|
||||
respawnEnabled: controller?.getConfig()?.enabled ?? false,
|
||||
respawnConfig: controller?.getConfig() ?? 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
|
||||
*/
|
||||
private getLightSessionsState() {
|
||||
return Array.from(this.sessions.values()).map(s => {
|
||||
const state = this.getSessionStateWithRespawn(s);
|
||||
return {
|
||||
...state,
|
||||
// Exclude full buffers - they're fetched on-demand
|
||||
terminalBuffer: '',
|
||||
textOutput: '',
|
||||
};
|
||||
});
|
||||
// getSessionStateWithRespawn already uses toLightDetailedState() which
|
||||
// excludes terminalBuffer and textOutput — no extra stripping needed
|
||||
return Array.from(this.sessions.values()).map(s => this.getSessionStateWithRespawn(s));
|
||||
}
|
||||
|
||||
// Clean up old completed scheduled runs
|
||||
|
||||
Reference in New Issue
Block a user