diff --git a/CLAUDE.md b/CLAUDE.md index 7e3486d0..409e25bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ When user says "COM": 1. Increment version in BOTH `package.json` AND `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.1436 (must match `package.json`) +**Version**: 0.1437 (must match `package.json`) ## Project Overview diff --git a/install.sh b/install.sh index 599bd60f..b969fb01 100755 --- a/install.sh +++ b/install.sh @@ -588,6 +588,50 @@ add_to_path() { success "Added to $profile - restart your shell or run: source $profile" } +# ============================================================================ +# Screen Configuration +# ============================================================================ + +setup_screenrc() { + local screenrc="$HOME/.screenrc" + + # Check if screenrc already exists and has our config + if [[ -f "$screenrc" ]] && grep -q "mousetrack on" "$screenrc" 2>/dev/null; then + info "Screen configuration already set up" + return 0 + fi + + info "Setting up screen configuration..." + + # Backup existing screenrc if it exists + if [[ -f "$screenrc" ]]; then + cp "$screenrc" "${screenrc}.backup.$(date +%Y%m%d%H%M%S)" + info "Backed up existing .screenrc" + fi + + # Create or append screen config + cat >> "$screenrc" << 'EOF' + +# ============================================================================ +# Claudeman screen configuration +# ============================================================================ + +# Enable mouse tracking for scrolling in mobile terminals (Termius, etc.) +mousetrack on + +# Increase scrollback buffer (default is 100) +defscrollback 10000 + +# Disable startup message +startup_message off + +# Enable alternate screen (better compatibility) +altscreen on +EOF + + success "Screen configuration saved to $screenrc" +} + # ============================================================================ # Systemd Service Setup (Linux only) # ============================================================================ @@ -838,6 +882,12 @@ main() { fi fi + # ======================================================================== + # Screen Configuration + # ======================================================================== + + setup_screenrc + # ======================================================================== # Systemd Service (Linux only) # ======================================================================== diff --git a/package.json b/package.json index 31ee265c..eb68964a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1436", + "version": "0.1437", "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", diff --git a/scripts/screen-chooser.sh b/scripts/screen-chooser.sh index dbbc753a..e05f47a1 100755 --- a/scripts/screen-chooser.sh +++ b/scripts/screen-chooser.sh @@ -140,34 +140,49 @@ find_full_session_id() { } # Get session name from Claudeman state or screens.json +# Falls back to working directory basename if name is empty get_session_name() { local session_id="$1" + local name="" + local workdir="" - # First, try screens.json (array format) - it has the name field + # First, try screens.json (array format) - it has both name and workingDir if [ -f "$CLAUDEMAN_SCREENS" ]; then - local name - name=$(jq -r --arg id "$session_id" ' - .[] | select(.sessionId | startswith($id)) | .name // empty + local result + result=$(jq -r --arg id "$session_id" ' + .[] | select(.sessionId | startswith($id)) | "\(.name // "")\t\(.workingDir // "")" ' "$CLAUDEMAN_SCREENS" 2>/dev/null | head -1) - if [ -n "$name" ] && [ "$name" != "null" ]; then - echo "$name" - return + if [ -n "$result" ]; then + name="${result%% *}" + workdir="${result#* }" fi fi - # Try state.json with full session ID lookup - if [ -f "$CLAUDEMAN_STATE" ]; then - local name - name=$(jq -r --arg id "$session_id" ' - .sessions | to_entries[] | select(.key | startswith($id)) | .value.name // empty + # If no name yet, try state.json + if [ -z "$name" ] && [ -f "$CLAUDEMAN_STATE" ]; then + local result + result=$(jq -r --arg id "$session_id" ' + .sessions | to_entries[] | select(.key | startswith($id)) | "\(.value.name // "")\t\(.value.workingDir // "")" ' "$CLAUDEMAN_STATE" 2>/dev/null | head -1) - if [ -n "$name" ] && [ "$name" != "null" ]; then - echo "$name" - return + if [ -n "$result" ]; then + name="${result%% *}" + [ -z "$workdir" ] && workdir="${result#* }" fi fi - # Fallback: use short ID + # If we have a name, use it + if [ -n "$name" ]; then + echo "$name" + return + fi + + # Fallback to working directory basename + if [ -n "$workdir" ]; then + echo "${workdir##*/}" + return + fi + + # Last resort: use short ID echo "${session_id:0:8}" } @@ -468,6 +483,7 @@ attach_screen() { local idx="$1" local pid="${SCREEN_PIDS[$idx]}" local name="${SCREEN_NAMES[$idx]}" + local state="${SCREEN_STATES[$idx]}" if [ -z "$pid" ]; then return 1 @@ -478,8 +494,13 @@ attach_screen() { echo -e "${D}(Ctrl+A D to detach)${R}" sleep 0.3 - # Attach - screen -r "$pid.$name" + # Use -x for attached screens (multi-display mode), -r for detached + # -x allows joining a screen that's already attached elsewhere + if [[ "$state" == "Attached" ]] || [[ "$state" == "Multi" ]]; then + screen -x "$pid.$name" + else + screen -r "$pid.$name" + fi # After detach, return to chooser return 0 diff --git a/src/subagent-watcher.ts b/src/subagent-watcher.ts index 2de69129..62b017c9 100644 --- a/src/subagent-watcher.ts +++ b/src/subagent-watcher.ts @@ -132,8 +132,13 @@ const MAX_TRACKED_AGENTS = 500; // Maximum agents to track (LRU eviction when ex const INTERNAL_AGENT_PATTERNS = [ /^\[?SUGGESTION MODE/i, // Claude Code's internal suggestion mode /^Suggest what user might/i, // Suggestion mode prompt variant + /^aprompt$/i, // Internal prompt agent + /^a\s?prompt/i, // Variants of internal prompt agent ]; +// Minimum description length - very short descriptions are likely internal or malformed +const MIN_DESCRIPTION_LENGTH = 5; + // Display/preview length constants const TEXT_PREVIEW_LENGTH = 200; // Length for text previews in tool results const USER_TEXT_PREVIEW_LENGTH = 80; // Length for user message previews @@ -165,9 +170,12 @@ export class SubagentWatcher extends EventEmitter { /** * Check if a description matches internal Claude Code agent patterns. * These are not real user-initiated subagents and should be filtered out. + * Also filters out very short descriptions that are likely internal or malformed. */ private isInternalAgent(description: string | undefined): boolean { if (!description) return false; + // Filter out very short descriptions (likely internal or malformed) + if (description.length < MIN_DESCRIPTION_LENGTH) return true; return INTERNAL_AGENT_PATTERNS.some(pattern => pattern.test(description)); } diff --git a/src/types.ts b/src/types.ts index 1cb98d72..563d4f50 100644 --- a/src/types.ts +++ b/src/types.ts @@ -107,6 +107,19 @@ export interface SessionConfig { createdAt: number; } +/** + * Available session colors for visual differentiation + */ +export type SessionColor = + | 'default' + | 'red' + | 'orange' + | 'yellow' + | 'green' + | 'blue' + | 'purple' + | 'pink'; + /** * Current state of a session */ @@ -163,6 +176,8 @@ export interface SessionState { niceEnabled?: boolean; /** Nice value (-20 to 19) */ niceValue?: number; + /** User-assigned color for visual differentiation */ + color?: SessionColor; } // ========== Global Stats Types ========== @@ -381,6 +396,26 @@ export interface RespawnConfig { aiPlanCheckCooldownMs?: number; } +/** + * Named respawn configuration preset for quick setup + */ +export interface RespawnPreset { + /** Unique preset identifier */ + id: string; + /** User-friendly preset name */ + name: string; + /** Description of when to use this preset */ + description?: string; + /** The respawn configuration (without enabled flag) */ + config: Omit; + /** Duration in minutes (optional default) */ + durationMinutes?: number; + /** Whether this is a built-in preset */ + builtIn?: boolean; + /** Timestamp when created */ + createdAt: number; +} + /** * Application configuration */ diff --git a/src/web/public/app.js b/src/web/public/app.js index 84dd6a39..56275474 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -685,8 +685,6 @@ class ClaudemanApp { // SSE connection status tracking this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; - this.lastDataReceived = Date.now(); - this.freshnessInterval = null; this.isOnline = navigator.onLine; // Accessibility: Focus trap for modals @@ -781,8 +779,6 @@ class ClaudemanApp { this.startSystemStatsPolling(); // Setup online/offline detection this.setupOnlineDetection(); - // Start freshness timer - this.startFreshnessTimer(); // Load server-stored settings (async, re-applies visibility after load) this.loadAppSettingsFromServer().then(() => { this.applyHeaderVisibilitySettings(); @@ -1274,13 +1270,8 @@ class ClaudemanApp { // Store all event listeners for cleanup on reconnect const listeners = []; const addListener = (event, handler) => { - // Wrap handler to mark data received on every SSE event - const wrappedHandler = (e) => { - this.markDataReceived(); - handler(e); - }; - this.eventSource.addEventListener(event, wrappedHandler); - listeners.push({ event, handler: wrappedHandler }); + this.eventSource.addEventListener(event, handler); + listeners.push({ event, handler }); }; // Create cleanup function to remove all listeners @@ -1295,7 +1286,6 @@ class ClaudemanApp { this.eventSource.onopen = () => { this.reconnectAttempts = 0; - this.lastDataReceived = Date.now(); this.setConnectionStatus('connected'); }; this.eventSource.onerror = () => { @@ -2197,35 +2187,8 @@ class ClaudemanApp { }); } - setConnectionStatus(status) { - const dot = document.querySelector('.status-dot'); - const text = document.querySelector('.status-text'); - if (!dot || !text) return; - - dot.className = 'status-dot ' + status; - - const statusLabels = { - connected: 'Connected', - connecting: 'Connecting...', - reconnecting: `Reconnecting (${this.reconnectAttempts}/${this.maxReconnectAttempts})`, - disconnected: 'Disconnected', - offline: 'Offline' - }; - text.textContent = statusLabels[status] || status; - - // Update title with more details - const container = document.getElementById('connectionStatus'); - if (container) { - if (status === 'reconnecting') { - container.title = `SSE reconnecting - attempt ${this.reconnectAttempts} of ${this.maxReconnectAttempts}`; - } else if (status === 'disconnected') { - container.title = 'SSE connection lost - max reconnection attempts reached'; - } else if (status === 'offline') { - container.title = 'Browser is offline - waiting for network'; - } else { - container.title = 'SSE connection status'; - } - } + setConnectionStatus(_status) { + // Connection status UI removed - method kept for compatibility } setupOnlineDetection() { @@ -2240,62 +2203,6 @@ class ClaudemanApp { }); } - startFreshnessTimer() { - // Clear existing interval if any - if (this.freshnessInterval) { - clearInterval(this.freshnessInterval); - } - - // Update freshness display every second - this.freshnessInterval = setInterval(() => { - this.updateFreshnessDisplay(); - }, 1000); - } - - updateFreshnessDisplay() { - const freshnessEl = document.getElementById('statusFreshness'); - if (!freshnessEl) return; - - const dot = document.querySelector('.status-dot'); - const isConnected = dot?.classList.contains('connected'); - - // Only show freshness when connected - if (!isConnected) { - freshnessEl.textContent = ''; - freshnessEl.className = 'status-freshness'; - return; - } - - const elapsed = Math.floor((Date.now() - this.lastDataReceived) / 1000); - - // Format the elapsed time - let timeText; - if (elapsed < 60) { - timeText = `${elapsed}s ago`; - } else if (elapsed < 3600) { - const mins = Math.floor(elapsed / 60); - timeText = `${mins}m ago`; - } else { - const hours = Math.floor(elapsed / 3600); - timeText = `${hours}h ago`; - } - - freshnessEl.textContent = timeText; - - // Apply staleness classes - freshnessEl.className = 'status-freshness'; - if (elapsed > 30) { - freshnessEl.classList.add('very-stale'); - } else if (elapsed > 10) { - freshnessEl.classList.add('stale'); - } - } - - // Call this when any SSE event is received to update lastDataReceived - markDataReceived() { - this.lastDataReceived = Date.now(); - } - handleInit(data) { // Update version display if (data.version) { diff --git a/src/web/public/index.html b/src/web/public/index.html index ce5f6ab3..6a48006a 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -13,6 +13,8 @@ + +
@@ -21,14 +23,14 @@
-
+
- - 14 - + + 14 +
@@ -46,11 +48,11 @@ --
- - +
0 tokens
@@ -62,7 +64,7 @@
0 tasks | $0.00
- + @@ -76,7 +78,7 @@ #0 - +
@@ -309,7 +311,7 @@