diff --git a/CLAUDE.md b/CLAUDE.md
index 236d2384..697ec3df 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.1433 (must match `package.json`)
+**Version**: 0.1434 (must match `package.json`)
## Project Overview
@@ -219,3 +219,26 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
## Deprecated Code
The TUI (Terminal UI) has been removed in favor of the web interface. Files in `src/tui/` are excluded from compilation via `tsconfig.json`.
+
+## Active Ralph Loop Task
+
+**Current Task**: built out the mac os installation script for claudeman, check if there is something available in your working directory and then optimize it and make a really clean, easy, simple and fully working installation script for claudeman for mac os x
+
+**Case Folder**: `/home/arkon/claudeman-cases/claudeman`
+
+### Key Files
+- **Plan Summary**: `/home/arkon/claudeman-cases/claudeman/ralph-wizard/summary.md` - Human-readable plan overview
+- **Todo Items**: `/home/arkon/claudeman-cases/claudeman/ralph-wizard/final-result.json` - Contains `items` array with all todo tasks
+- **Research**: `/home/arkon/claudeman-cases/claudeman/ralph-wizard/research/result.json` - External resources and codebase patterns
+
+### How to Work on This Task
+1. Read the plan summary to understand the overall approach
+2. Check `final-result.json` for the todo items array - each item has `id`, `title`, `description`, `priority`
+3. Work through items in priority order (critical → high → medium → low)
+4. Use `COMPLETION_PHRASE` when the entire task is complete
+
+### Research Insights
+Check `/home/arkon/claudeman-cases/claudeman/ralph-wizard/research/result.json` for:
+- External GitHub repos and documentation links to reference
+- Existing codebase patterns to follow
+- Technical recommendations from the research phase
diff --git a/check-tabs.js b/check-tabs.js
new file mode 100644
index 00000000..d0daea19
--- /dev/null
+++ b/check-tabs.js
@@ -0,0 +1,83 @@
+const { chromium } = require("playwright");
+
+(async () => {
+ const browser = await chromium.launch({ headless: true });
+ const page = await browser.newPage();
+ page.setDefaultTimeout(10000);
+
+ await page.goto("http://localhost:3199", { waitUntil: "domcontentloaded" });
+ await page.waitForTimeout(3000);
+
+ // Click first session tab
+ await page.locator(".session-tab").first().click();
+ await page.waitForTimeout(500);
+
+ // Open session options
+ await page.locator(".session-tab.active .tab-gear").click();
+ await page.waitForTimeout(500);
+
+ // Function to check VISIBILITY of summary elements
+ async function checkSummaryVisibility(tabName) {
+ console.log("\n=== " + tabName.toUpperCase() + " TAB ===");
+
+ // Check if summary-specific elements are VISIBLE (not just in DOM)
+
+ const summarySection = page.locator(".modal:visible #summary-section, .modal:visible [id*='summary']");
+ const summaryVisible = await summarySection.first().isVisible().catch(() => false);
+
+ const timelineSection = page.locator(".modal:visible .run-timeline, .modal:visible [class*='timeline']");
+ const timelineVisible = await timelineSection.first().isVisible().catch(() => false);
+
+ const filterButtons = page.locator(".modal:visible .filter-btn, .modal:visible [class*='filter']");
+ const filterVisible = await filterButtons.first().isVisible().catch(() => false);
+
+ const exportBtns = page.locator(".modal:visible .export-btn, .modal:visible button:has-text('Copy'), .modal:visible button:has-text('Download')");
+ const exportVisible = await exportBtns.first().isVisible().catch(() => false);
+
+ console.log(" Summary section visible:", summaryVisible);
+ console.log(" Timeline visible:", timelineVisible);
+ console.log(" Filter buttons visible:", filterVisible);
+ console.log(" Export buttons visible:", exportVisible);
+
+ return { summaryVisible, timelineVisible, filterVisible, exportVisible };
+ }
+
+ // Check Respawn tab (default)
+ const respawn = await checkSummaryVisibility("Respawn");
+
+ // Check Context tab
+ await page.locator(".modal:visible .modal-tab-btn[data-tab='context']").click();
+ await page.waitForTimeout(200);
+ const context = await checkSummaryVisibility("Context");
+
+ // Check Ralph tab
+ await page.locator(".modal:visible .modal-tab-btn[data-tab='ralph']").click();
+ await page.waitForTimeout(200);
+ const ralph = await checkSummaryVisibility("Ralph");
+
+ // Check Summary tab
+ await page.locator(".modal:visible .modal-tab-btn[data-tab='summary']").click();
+ await page.waitForTimeout(200);
+ const summary = await checkSummaryVisibility("Summary");
+
+ await browser.close();
+
+ console.log("\n=== FINAL BUG ASSESSMENT ===");
+ const hasBugRespawn = respawn.summaryVisible || respawn.timelineVisible;
+ const hasBugContext = context.summaryVisible || context.timelineVisible;
+ const hasBugRalph = ralph.summaryVisible || ralph.timelineVisible;
+ const summaryWorking = summary.summaryVisible || summary.timelineVisible;
+
+ console.log("Respawn tab leaking summary content:", hasBugRespawn ? "YES - BUG EXISTS" : "No - Fixed");
+ console.log("Context tab leaking summary content:", hasBugContext ? "YES - BUG EXISTS" : "No - Fixed");
+ console.log("Ralph tab leaking summary content:", hasBugRalph ? "YES - BUG EXISTS" : "No - Fixed");
+ console.log("Summary tab showing its content:", summaryWorking ? "Yes - Working correctly" : "No - Check if empty or hidden");
+
+ const bugFixed = (hasBugRespawn === false) && (hasBugContext === false) && (hasBugRalph === false);
+ if (bugFixed) {
+ console.log("\n*** BUG IS FIXED - Summary content only appears in Summary tab ***");
+ } else {
+ console.log("\n*** BUG STILL EXISTS - Summary content bleeding to other tabs ***");
+ }
+
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/package.json b/package.json
index 5c2ed1f5..7c52ca93 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "claudeman",
- "version": "0.1433",
+ "version": "0.1434",
"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
new file mode 100755
index 00000000..dbbc753a
--- /dev/null
+++ b/scripts/screen-chooser.sh
@@ -0,0 +1,691 @@
+#!/bin/bash
+# ============================================================================
+# Screen Chooser for iPhone/Termius
+# Optimized for iPhone 17 Pro (portrait ~45 chars, landscape ~95 chars)
+# ============================================================================
+#
+# Design principles:
+# - Single-digit selection (1-9) for fast thumb typing
+# - Compact display, no wasted space
+# - Color-coded status for quick scanning
+# - Names pulled from Claudeman state.json
+# - Minimal keystrokes to attach
+#
+# Usage:
+# ./screen-chooser.sh # Interactive chooser
+# ./screen-chooser.sh 1 # Quick attach to session 1
+# ./screen-chooser.sh -l # List only (no interactive)
+# ./screen-chooser.sh -h # Help
+#
+# Alias: alias sc='path/to/screen-chooser.sh'
+# Then: sc (interactive)
+# sc 2 (attach session 2)
+#
+# ============================================================================
+
+set -e
+
+# ============================================================================
+# Configuration
+# ============================================================================
+
+CLAUDEMAN_STATE="$HOME/.claudeman/state.json"
+CLAUDEMAN_SCREENS="$HOME/.claudeman/screens.json"
+
+# iPhone 17 Pro portrait width (conservative)
+MAX_WIDTH=44
+MAX_NAME_LEN=28
+
+# Page size for pagination (leave room for header/footer)
+PAGE_SIZE=7
+
+# Auto-refresh timeout (seconds) - 0 to disable
+AUTO_REFRESH=60
+
+# ============================================================================
+# Icon Detection (Nerd Fonts vs ASCII)
+# ============================================================================
+
+# Detect if terminal likely supports nerd fonts
+# Termius generally doesn't, so default to ASCII
+detect_icons() {
+ # Check TERM and common indicators
+ if [[ "$TERM_PROGRAM" == "iTerm"* ]] || \
+ [[ "$TERM" == "xterm-kitty" ]] || \
+ [[ -n "$WEZTERM_PANE" ]] || \
+ [[ "$LC_TERMINAL" == "iTerm2" ]]; then
+ ICON_SCREEN=""
+ ICON_ATTACHED="●"
+ ICON_DETACHED="○"
+ ICON_UNKNOWN="◌"
+ else
+ # ASCII fallback for Termius/basic terminals
+ ICON_SCREEN="[S]"
+ ICON_ATTACHED="*"
+ ICON_DETACHED="-"
+ ICON_UNKNOWN="?"
+ fi
+}
+
+detect_icons
+
+# ============================================================================
+# Colors - ANSI 256 for better Termius compatibility
+# ============================================================================
+
+R='\033[0m' # Reset
+B='\033[1m' # Bold
+D='\033[2m' # Dim
+GREEN='\033[38;5;82m'
+YELLOW='\033[38;5;220m'
+BLUE='\033[38;5;75m'
+CYAN='\033[38;5;87m'
+RED='\033[38;5;203m'
+GRAY='\033[38;5;245m'
+WHITE='\033[38;5;255m'
+BG_SEL='\033[48;5;236m' # Selection background
+
+# ============================================================================
+# Utilities
+# ============================================================================
+
+# Truncate string with ellipsis, keeping end if it looks like a path
+truncate() {
+ local str="$1"
+ local max="$2"
+ local len=${#str}
+
+ if [ "$len" -le "$max" ]; then
+ echo "$str"
+ return
+ fi
+
+ # If it's a path-like string, keep the end
+ if [[ "$str" == *"/"* ]]; then
+ echo "..${str: -$((max-2))}"
+ else
+ echo "${str:0:$((max-1))}…"
+ fi
+}
+
+# Find full session ID from short ID (screen names use first 8 chars)
+find_full_session_id() {
+ local short_id="$1"
+
+ # Try state.json first
+ if [ -f "$CLAUDEMAN_STATE" ]; then
+ local full_id
+ full_id=$(jq -r --arg short "$short_id" '
+ .sessions | keys[] | select(startswith($short))
+ ' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
+ if [ -n "$full_id" ]; then
+ echo "$full_id"
+ return
+ fi
+ fi
+
+ # Try screens.json array
+ if [ -f "$CLAUDEMAN_SCREENS" ]; then
+ local full_id
+ full_id=$(jq -r --arg short "$short_id" '
+ .[] | select(.sessionId | startswith($short)) | .sessionId
+ ' "$CLAUDEMAN_SCREENS" 2>/dev/null | head -1)
+ if [ -n "$full_id" ]; then
+ echo "$full_id"
+ return
+ fi
+ fi
+
+ echo "$short_id"
+}
+
+# Get session name from Claudeman state or screens.json
+get_session_name() {
+ local session_id="$1"
+
+ # First, try screens.json (array format) - it has the name field
+ if [ -f "$CLAUDEMAN_SCREENS" ]; then
+ local name
+ name=$(jq -r --arg id "$session_id" '
+ .[] | select(.sessionId | startswith($id)) | .name // empty
+ ' "$CLAUDEMAN_SCREENS" 2>/dev/null | head -1)
+ if [ -n "$name" ] && [ "$name" != "null" ]; then
+ echo "$name"
+ return
+ 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
+ ' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
+ if [ -n "$name" ] && [ "$name" != "null" ]; then
+ echo "$name"
+ return
+ fi
+ fi
+
+ # Fallback: use short ID
+ echo "${session_id:0:8}"
+}
+
+# Get working directory from state or screens.json
+get_working_dir() {
+ local session_id="$1"
+
+ # Try screens.json first (array format)
+ if [ -f "$CLAUDEMAN_SCREENS" ]; then
+ local dir
+ dir=$(jq -r --arg id "$session_id" '
+ .[] | select(.sessionId | startswith($id)) | .workingDir // empty
+ ' "$CLAUDEMAN_SCREENS" 2>/dev/null | head -1)
+ if [ -n "$dir" ] && [ "$dir" != "null" ]; then
+ echo "${dir/#$HOME/~}"
+ return
+ fi
+ fi
+
+ # Try state.json
+ if [ -f "$CLAUDEMAN_STATE" ]; then
+ local dir
+ dir=$(jq -r --arg id "$session_id" '
+ .sessions | to_entries[] | select(.key | startswith($id)) | .value.workingDir // empty
+ ' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
+ if [ -n "$dir" ] && [ "$dir" != "null" ]; then
+ echo "${dir/#$HOME/~}"
+ return
+ fi
+ fi
+ echo ""
+}
+
+# Get token count from state
+get_tokens() {
+ local session_id="$1"
+
+ if [ -f "$CLAUDEMAN_STATE" ]; then
+ local tokens
+ tokens=$(jq -r --arg id "$session_id" '
+ .sessions | to_entries[] | select(.key | startswith($id)) |
+ ((.value.inputTokens // 0) + (.value.outputTokens // 0))
+ ' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
+
+ if [ -n "$tokens" ] && [ "$tokens" != "null" ] && [ "$tokens" -gt 0 ] 2>/dev/null; then
+ if [ "$tokens" -gt 1000 ]; then
+ echo "$((tokens / 1000))k"
+ else
+ echo "${tokens}"
+ fi
+ return
+ fi
+ fi
+ echo ""
+}
+
+# Get respawn status from screens.json (array format)
+get_respawn_status() {
+ local session_id="$1"
+
+ if [ -f "$CLAUDEMAN_SCREENS" ]; then
+ local respawn_enabled
+ respawn_enabled=$(jq -r --arg id "$session_id" '
+ .[] | select(.sessionId | startswith($id)) | .respawnConfig.enabled // false
+ ' "$CLAUDEMAN_SCREENS" 2>/dev/null | head -1)
+
+ if [ "$respawn_enabled" = "true" ]; then
+ echo "R" # Respawn active
+ return
+ fi
+ fi
+ echo ""
+}
+
+# Check if jq is available
+check_deps() {
+ if ! command -v jq &>/dev/null; then
+ echo -e "${YELLOW}Note: Install jq for session names${R}"
+ echo ""
+ fi
+}
+
+# ============================================================================
+# Screen List Parser
+# ============================================================================
+
+declare -a SCREEN_PIDS
+declare -a SCREEN_NAMES
+declare -a SCREEN_STATES
+declare -a SESSION_IDS
+declare -a DISPLAY_NAMES
+declare -a WORKING_DIRS
+declare -a TOKEN_COUNTS
+declare -a RESPAWN_STATUS
+
+parse_screens() {
+ SCREEN_PIDS=()
+ SCREEN_NAMES=()
+ SCREEN_STATES=()
+ SESSION_IDS=()
+ DISPLAY_NAMES=()
+ WORKING_DIRS=()
+ TOKEN_COUNTS=()
+ RESPAWN_STATUS=()
+
+ local i=0
+
+ # Regex for screen -ls lines: " 12345.claudeman-abc123 (date) (Attached)"
+ # We need to capture: PID, name, and the LAST parenthesized group (state)
+ # Modern screen -ls shows: PID.name (date) (Attached|Detached)
+ local screen_regex='^[[:space:]]*([0-9]+)\.([^[:space:]]+).*\((Attached|Detached|Multi)\)'
+
+ # Parse screen -ls output
+ while IFS= read -r line; do
+ if [[ "$line" =~ $screen_regex ]]; then
+ local pid="${BASH_REMATCH[1]}"
+ local name="${BASH_REMATCH[2]}"
+ local state="${BASH_REMATCH[3]}"
+
+ SCREEN_PIDS+=("$pid")
+ SCREEN_NAMES+=("$name")
+ SCREEN_STATES+=("$state")
+
+ # Extract session ID from claudeman screen name
+ local session_id=""
+ local cm_regex='^claudeman-(.+)$'
+ if [[ "$name" =~ $cm_regex ]]; then
+ session_id="${BASH_REMATCH[1]}"
+ fi
+ SESSION_IDS+=("$session_id")
+
+ # Get display name and metadata
+ if [ -n "$session_id" ]; then
+ DISPLAY_NAMES+=("$(get_session_name "$session_id")")
+ WORKING_DIRS+=("$(get_working_dir "$session_id")")
+ TOKEN_COUNTS+=("$(get_tokens "$session_id")")
+ RESPAWN_STATUS+=("$(get_respawn_status "$session_id")")
+ else
+ DISPLAY_NAMES+=("$name")
+ WORKING_DIRS+=("")
+ TOKEN_COUNTS+=("")
+ RESPAWN_STATUS+=("")
+ fi
+
+ i=$((i + 1))
+ fi
+ done < <(screen -ls 2>/dev/null || true)
+}
+
+# ============================================================================
+# Display Functions
+# ============================================================================
+
+clear_screen() {
+ printf '\033[2J\033[H'
+}
+
+# Print header
+print_header() {
+ local count=${#SCREEN_PIDS[@]}
+ echo -e "${B}${CYAN}${ICON_SCREEN} Screens${R} ${D}($count)${R}"
+ echo -e "${D}$(printf '%.0s─' {1..32})${R}"
+}
+
+# Print a session entry
+print_entry() {
+ local idx="$1"
+ local num=$((idx + 1))
+ local name="${DISPLAY_NAMES[$idx]}"
+ local state="${SCREEN_STATES[$idx]}"
+ local dir="${WORKING_DIRS[$idx]}"
+ local tokens="${TOKEN_COUNTS[$idx]}"
+ local respawn="${RESPAWN_STATUS[$idx]}"
+
+ # Truncate name (leave room for indicators)
+ local name_max=$MAX_NAME_LEN
+ [ -n "$respawn" ] && name_max=$((name_max - 2))
+ [ -n "$tokens" ] && name_max=$((name_max - 4))
+ name=$(truncate "$name" $name_max)
+
+ # Status indicator and color
+ local status_icon status_color
+ if [[ "$state" == *"Attached"* ]]; then
+ status_icon="$ICON_ATTACHED"
+ status_color="$GREEN"
+ elif [[ "$state" == *"Detached"* ]]; then
+ status_icon="$ICON_DETACHED"
+ status_color="$GRAY"
+ else
+ status_icon="$ICON_UNKNOWN"
+ status_color="$YELLOW"
+ fi
+
+ # Build the line
+ # Format: "1) name ○ R 45k"
+ local num_str="${B}${WHITE}${num})${R}"
+ local name_str="${B}${WHITE}${name}${R}"
+ local status_str="${status_color}${status_icon}${R}"
+
+ # Respawn indicator (green R if active)
+ local respawn_str=""
+ if [ -n "$respawn" ]; then
+ respawn_str=" ${GREEN}${respawn}${R}"
+ fi
+
+ # Token display
+ local token_str=""
+ if [ -n "$tokens" ]; then
+ token_str=" ${D}${tokens}${R}"
+ fi
+
+ echo -e " ${num_str} ${name_str} ${status_str}${respawn_str}${token_str}"
+
+ # Show directory on second line if present (dimmed, indented)
+ if [ -n "$dir" ]; then
+ dir=$(truncate "$dir" $((MAX_NAME_LEN - 2)))
+ echo -e " ${D}${dir}${R}"
+ fi
+}
+
+# Print footer with commands
+print_footer() {
+ local page="$1"
+ local total_pages="$2"
+
+ echo ""
+ echo -e "${D}────────────────────────────────${R}"
+
+ # Pagination indicator
+ if [ "$total_pages" -gt 1 ]; then
+ echo -e " ${D}Page $((page+1))/$total_pages${R} ${GRAY}[${WHITE}n${GRAY}]ext [${WHITE}p${GRAY}]rev${R}"
+ fi
+
+ # Commands - most used first, thumb-friendly
+ echo -e " ${GRAY}[${WHITE}1-9${GRAY}]attach [${WHITE}r${GRAY}]efresh [${WHITE}q${GRAY}]uit${R}"
+}
+
+# Print no screens message
+print_no_screens() {
+ clear_screen
+ echo -e "${B}${CYAN}${ICON_SCREEN} Screens${R}"
+ echo -e "${D}$(printf '%.0s─' {1..32})${R}"
+ echo ""
+ echo -e " ${YELLOW}No screen sessions found${R}"
+ echo ""
+ echo -e " ${D}Start one with:${R}"
+ echo -e " ${WHITE}claudeman web${R}"
+ echo ""
+ echo -e "${D}$(printf '%.0s─' {1..32})${R}"
+ echo -e " ${GRAY}[${WHITE}r${GRAY}]efresh [${WHITE}q${GRAY}]uit${R}"
+}
+
+# ============================================================================
+# Main Display Loop
+# ============================================================================
+
+current_page=0
+
+render() {
+ clear_screen
+ parse_screens
+
+ local count=${#SCREEN_PIDS[@]}
+
+ if [ "$count" -eq 0 ]; then
+ print_no_screens
+ return
+ fi
+
+ local total_pages=$(( (count + PAGE_SIZE - 1) / PAGE_SIZE ))
+
+ # Clamp page
+ if [ "$current_page" -ge "$total_pages" ]; then
+ current_page=$((total_pages - 1))
+ fi
+ if [ "$current_page" -lt 0 ]; then
+ current_page=0
+ fi
+
+ local start=$((current_page * PAGE_SIZE))
+ local end=$((start + PAGE_SIZE))
+ if [ "$end" -gt "$count" ]; then
+ end=$count
+ fi
+
+ print_header
+ echo ""
+
+ for ((i = start; i < end; i++)); do
+ print_entry $i
+ done
+
+ print_footer $current_page $total_pages
+}
+
+# Attach to a screen
+attach_screen() {
+ local idx="$1"
+ local pid="${SCREEN_PIDS[$idx]}"
+ local name="${SCREEN_NAMES[$idx]}"
+
+ if [ -z "$pid" ]; then
+ return 1
+ fi
+
+ clear_screen
+ echo -e "${GREEN}Attaching to ${B}${DISPLAY_NAMES[$idx]}${R}${GREEN}...${R}"
+ echo -e "${D}(Ctrl+A D to detach)${R}"
+ sleep 0.3
+
+ # Attach
+ screen -r "$pid.$name"
+
+ # After detach, return to chooser
+ return 0
+}
+
+# ============================================================================
+# Input Handler
+# ============================================================================
+
+handle_input() {
+ local key="$1"
+ local count=${#SCREEN_PIDS[@]}
+ local total_pages=$(( (count + PAGE_SIZE - 1) / PAGE_SIZE ))
+
+ case "$key" in
+ # Number selection (1-9)
+ [1-9])
+ local idx=$((key - 1))
+ if [ "$idx" -lt "$count" ]; then
+ attach_screen "$idx"
+ return 0
+ fi
+ ;;
+
+ # Escape sequence (arrow keys)
+ $'\e')
+ # Read the rest of the escape sequence
+ read -rsn2 -t 0.1 seq 2>/dev/null || true
+ case "$seq" in
+ '[A'|'[D') # Up or Left arrow
+ if [ "$total_pages" -gt 1 ]; then
+ current_page=$(( (current_page - 1 + total_pages) % total_pages ))
+ fi
+ ;;
+ '[B'|'[C') # Down or Right arrow
+ if [ "$total_pages" -gt 1 ]; then
+ current_page=$(( (current_page + 1) % total_pages ))
+ fi
+ ;;
+ esac
+ ;;
+
+ # Navigation (vim-style and standard)
+ n|N|j|J)
+ if [ "$total_pages" -gt 1 ]; then
+ current_page=$(( (current_page + 1) % total_pages ))
+ fi
+ ;;
+
+ p|P|k|K)
+ if [ "$total_pages" -gt 1 ]; then
+ current_page=$(( (current_page - 1 + total_pages) % total_pages ))
+ fi
+ ;;
+
+ # Refresh
+ r|R)
+ # Just re-render
+ ;;
+
+ # Quit
+ q|Q)
+ clear_screen
+ exit 0
+ ;;
+
+ # Enter with no selection - attach first if only one
+ '')
+ if [ "$count" -eq 1 ]; then
+ attach_screen 0
+ return 0
+ fi
+ ;;
+ esac
+
+ return 0
+}
+
+# ============================================================================
+# List Mode (non-interactive, for scripting)
+# ============================================================================
+
+list_mode() {
+ parse_screens
+ local count=${#SCREEN_PIDS[@]}
+
+ if [ "$count" -eq 0 ]; then
+ echo "No screen sessions"
+ exit 0
+ fi
+
+ for ((i = 0; i < count; i++)); do
+ local num=$((i + 1))
+ local name="${DISPLAY_NAMES[$i]}"
+ local state="${SCREEN_STATES[$i]}"
+ local respawn="${RESPAWN_STATUS[$i]}"
+ local indicator="-"
+ [[ "$state" == *"Attached"* ]] && indicator="*"
+ [ -n "$respawn" ] && indicator="${indicator}R"
+
+ echo "$num) $name [$indicator]"
+ done
+}
+
+# ============================================================================
+# Quick Attach (by number)
+# ============================================================================
+
+quick_attach() {
+ local num="$1"
+ parse_screens
+
+ local count=${#SCREEN_PIDS[@]}
+ local idx=$((num - 1))
+
+ if [ "$idx" -lt 0 ] || [ "$idx" -ge "$count" ]; then
+ echo -e "${RED}Invalid session: $num${R}"
+ echo "Available: 1-$count"
+ exit 1
+ fi
+
+ attach_screen "$idx"
+}
+
+# ============================================================================
+# Help
+# ============================================================================
+
+show_help() {
+ cat << 'EOF'
+Screen Chooser for iPhone/Termius
+
+USAGE:
+ sc Interactive chooser
+ sc Quick attach to session N
+ sc -l List sessions (non-interactive)
+ sc -h Show this help
+
+INTERACTIVE KEYS:
+ 1-9 Attach to session
+ n/j/↓ Next page
+ p/k/↑ Previous page
+ r Refresh
+ q Quit
+
+INDICATORS:
+ * / ● Attached (someone connected)
+ - / ○ Detached (available)
+ R Respawn enabled
+ 45k Token count
+
+TIPS:
+ - Alias: alias sc='path/to/screen-chooser.sh'
+ - Detach: Ctrl+A D
+ - Session names from Claudeman state
+
+EOF
+}
+
+# ============================================================================
+# Main
+# ============================================================================
+
+main() {
+ # Argument parsing
+ case "${1:-}" in
+ -h|--help)
+ show_help
+ exit 0
+ ;;
+ -l|--list)
+ list_mode
+ exit 0
+ ;;
+ [1-9]|[1-9][0-9])
+ # Quick attach by number
+ quick_attach "$1"
+ exit $?
+ ;;
+ esac
+
+ # Check dependencies
+ check_deps
+
+ # Initial render
+ render
+
+ # Input loop
+ while true; do
+ # Read single character without enter
+ # -s: silent, -n1: one char, -t: timeout for auto-refresh
+ local timeout_opt=""
+ if [ "$AUTO_REFRESH" -gt 0 ]; then
+ timeout_opt="-t $AUTO_REFRESH"
+ fi
+
+ if read -rsn1 $timeout_opt key 2>/dev/null; then
+ handle_input "$key"
+ fi
+ # Re-render after input or timeout (auto-refresh)
+ render
+ done
+}
+
+# Handle Ctrl+C gracefully
+trap 'clear_screen; exit 0' INT
+
+# Run
+main "$@"
diff --git a/src/bash-tool-parser.ts b/src/bash-tool-parser.ts
index b9ae6d73..cb37f8d4 100644
--- a/src/bash-tool-parser.ts
+++ b/src/bash-tool-parser.ts
@@ -145,8 +145,11 @@ export class BashToolParser extends EventEmitter {
private _workingDir: string;
private _homeDir: string;
- // Track normalized paths to detect equivalents (normalized -> original)
- private _normalizedPathMap: Map = new Map();
+ // Track auto-remove timers for cleanup
+ private _autoRemoveTimers: Set> = new Set();
+
+ // Flag to prevent operations after destroy
+ private _destroyed: boolean = false;
// Debouncing
private _pendingUpdate: boolean = false;
@@ -395,7 +398,6 @@ export class BashToolParser extends EventEmitter {
*/
reset(): void {
this._activeTools.clear();
- this._normalizedPathMap.clear();
this._lineBuffer = '';
this._lastToolId = null;
this.emitUpdate();
@@ -408,7 +410,7 @@ export class BashToolParser extends EventEmitter {
* @param data - Raw terminal data (may include ANSI codes)
*/
processTerminalData(data: string): void {
- if (!this._enabled) return;
+ if (!this._enabled || this._destroyed) return;
// Append to line buffer
this._lineBuffer += data;
@@ -496,10 +498,13 @@ export class BashToolParser extends EventEmitter {
this.scheduleUpdate();
// Remove completed tool after a short delay to allow UI to show completion
- setTimeout(() => {
+ const timer = setTimeout(() => {
+ this._autoRemoveTimers.delete(timer);
+ if (this._destroyed) return;
this._activeTools.delete(tool.id);
this.scheduleUpdate();
}, 2000);
+ this._autoRemoveTimers.add(timer);
}
this._lastToolId = null;
return;
@@ -531,10 +536,13 @@ export class BashToolParser extends EventEmitter {
this.scheduleUpdate();
// Auto-remove suggestions after 30 seconds
- setTimeout(() => {
+ const timer = setTimeout(() => {
+ this._autoRemoveTimers.delete(timer);
+ if (this._destroyed) return;
this._activeTools.delete(tool.id);
this.scheduleUpdate();
}, 30000);
+ this._autoRemoveTimers.add(timer);
return;
}
@@ -565,10 +573,13 @@ export class BashToolParser extends EventEmitter {
this.scheduleUpdate();
// Auto-remove after 60 seconds
- setTimeout(() => {
+ const timer = setTimeout(() => {
+ this._autoRemoveTimers.delete(timer);
+ if (this._destroyed) return;
this._activeTools.delete(tool.id);
this.scheduleUpdate();
}, 60000);
+ this._autoRemoveTimers.add(timer);
}
}
@@ -657,12 +668,17 @@ export class BashToolParser extends EventEmitter {
* Clean up resources.
*/
destroy(): void {
+ this._destroyed = true;
if (this._updateTimer) {
clearTimeout(this._updateTimer);
this._updateTimer = null;
}
+ // Clear all auto-remove timers to prevent orphaned callbacks
+ for (const timer of this._autoRemoveTimers) {
+ clearTimeout(timer);
+ }
+ this._autoRemoveTimers.clear();
this._activeTools.clear();
- this._normalizedPathMap.clear();
this.removeAllListeners();
}
}
diff --git a/src/file-stream-manager.ts b/src/file-stream-manager.ts
index f7aa8cb8..d1a9bf13 100644
--- a/src/file-stream-manager.ts
+++ b/src/file-stream-manager.ts
@@ -329,6 +329,10 @@ export class FileStreamManager extends EventEmitter {
for (const streamId of this.streams.keys()) {
this.closeStream(streamId);
}
+
+ // Clear Maps to release references
+ this.streams.clear();
+ this.sessionStreamCounts.clear();
}
// ========== Private Methods ==========
diff --git a/src/image-watcher.ts b/src/image-watcher.ts
index 93bc75ab..15e6f273 100644
--- a/src/image-watcher.ts
+++ b/src/image-watcher.ts
@@ -67,9 +67,12 @@ export class ImageWatcher extends EventEmitter {
/** Map of sessionId -> working directory path */
private sessionDirs = new Map();
- /** Debounce timers for rapid image creation */
+ /** Debounce timers for rapid image creation (keyed by filePath) */
private debounceTimers = new Map();
+ /** Track which session owns each debounce timer (for cleanup) */
+ private timerToSession = new Map();
+
/** Whether the watcher is currently running */
private _isRunning = false;
@@ -117,6 +120,7 @@ export class ImageWatcher extends EventEmitter {
clearTimeout(timer);
}
this.debounceTimers.clear();
+ this.timerToSession.clear();
}
/**
@@ -199,11 +203,21 @@ export class ImageWatcher extends EventEmitter {
}
this.sessionDirs.delete(sessionId);
- // Clear any pending debounce timer for this session
- const timer = this.debounceTimers.get(sessionId);
- if (timer) {
- clearTimeout(timer);
- this.debounceTimers.delete(sessionId);
+ // Clear any pending debounce timers for this session
+ // Collect keys first to avoid iterator invalidation during deletion
+ const toDelete: string[] = [];
+ for (const [filePath, ownerId] of this.timerToSession) {
+ if (ownerId === sessionId) {
+ toDelete.push(filePath);
+ }
+ }
+ for (const filePath of toDelete) {
+ const timer = this.debounceTimers.get(filePath);
+ if (timer) {
+ clearTimeout(timer);
+ this.debounceTimers.delete(filePath);
+ }
+ this.timerToSession.delete(filePath);
}
}
@@ -236,10 +250,12 @@ export class ImageWatcher extends EventEmitter {
const timer = setTimeout(() => {
this.debounceTimers.delete(filePath);
+ this.timerToSession.delete(filePath);
this.emitImageDetected(sessionId, filePath);
}, DEBOUNCE_DELAY_MS);
this.debounceTimers.set(filePath, timer);
+ this.timerToSession.set(filePath, sessionId);
}
/**
diff --git a/src/ralph-tracker.ts b/src/ralph-tracker.ts
index b11e15b7..083aaca0 100644
--- a/src/ralph-tracker.ts
+++ b/src/ralph-tracker.ts
@@ -1882,6 +1882,8 @@ export class RalphTracker extends EventEmitter {
clear(): void {
// Clear debounce timers to prevent stale emissions after clear
this.clearDebounceTimers();
+ // Stop fix plan file watcher to prevent memory leak
+ this.stopWatchingFixPlan();
this._loopState = createInitialRalphTrackerState(); // This sets enabled: false
this._todos.clear();
this._taskNumberToContent.clear();
@@ -2765,4 +2767,20 @@ export class RalphTracker extends EventEmitter {
return this._checkpointIterations.includes(currentIteration) &&
currentIteration > this._lastCheckpointIteration;
}
+
+ /**
+ * Clean up all resources and release memory.
+ *
+ * Call this when the session is being destroyed to prevent memory leaks.
+ * Stops file watchers, clears all timers, data, and removes event listeners.
+ */
+ destroy(): void {
+ this.clearDebounceTimers();
+ this.stopWatchingFixPlan();
+ this._todos.clear();
+ this._taskNumberToContent.clear();
+ this._completionPhraseCount.clear();
+ this._planTasks.clear();
+ this.removeAllListeners();
+ }
}
diff --git a/src/session.ts b/src/session.ts
index 66566d9a..435fd015 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -80,6 +80,14 @@ const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
// We look for the tool name followed by (description)
const TASK_TOOL_PATTERN = /\b(Explore|Task|Bash|Plan|general-purpose)\(([^)]+)\)/g;
+// Pre-compiled patterns for hot paths (avoid regex compilation per call)
+/** Pattern to strip leading ANSI escapes and whitespace from terminal buffer */
+const LEADING_ANSI_WHITESPACE_PATTERN = /^(\x1b\[\??[\d;]*[A-Za-z]|[\s\r\n])+/;
+/** Pattern to match Ctrl+L (form feed) characters */
+const CTRL_L_PATTERN = /\x0c/g;
+/** Pattern to split by newlines (CR or LF) */
+const NEWLINE_SPLIT_PATTERN = /\r?\n/;
+
// ============================================================================
// Claude CLI PATH Resolution
// ============================================================================
@@ -297,7 +305,6 @@ export class Session extends EventEmitter {
private _currentTaskId: string | null = null;
// Use BufferAccumulator for hot-path buffers to reduce GC pressure
private _terminalBuffer = new BufferAccumulator(MAX_TERMINAL_BUFFER_SIZE, TERMINAL_BUFFER_TRIM_SIZE);
- private _outputBuffer: string = '';
private _textOutput = new BufferAccumulator(MAX_TEXT_OUTPUT_SIZE, TEXT_OUTPUT_TRIM_SIZE);
private _errorBuffer: string = '';
private _lastActivityAt: number;
@@ -469,10 +476,6 @@ export class Session extends EventEmitter {
return this._terminalBuffer.value;
}
- get outputBuffer(): string {
- return this._outputBuffer;
- }
-
get textOutput(): string {
return this._textOutput.value;
}
@@ -787,7 +790,6 @@ export class Session extends EventEmitter {
this._status = 'busy';
this._terminalBuffer.clear();
- this._outputBuffer = '';
this._textOutput.clear();
this._errorBuffer = '';
this._messages = [];
@@ -852,7 +854,7 @@ export class Session extends EventEmitter {
// Strip: cursor movement (\x1b[nA/B/C/D), positioning (\x1b[n;nH),
// clear screen (\x1b[2J), scroll region (\x1b[n;nr), and whitespace
this._terminalBuffer.set(
- bufferValue.replace(/^(\x1b\[\??[\d;]*[A-Za-z]|[\s\r\n])+/, '')
+ bufferValue.replace(LEADING_ANSI_WHITESPACE_PATTERN, '')
);
// Signal client to refresh
this.emit('clearTerminal');
@@ -916,7 +918,7 @@ export class Session extends EventEmitter {
// Filter out focus escape sequences and Ctrl+L (form feed)
const data = rawData
.replace(FOCUS_ESCAPE_FILTER, '')
- .replace(/\x0c/g, ''); // Remove Ctrl+L
+ .replace(CTRL_L_PATTERN, ''); // Remove Ctrl+L
if (!data) return; // Skip if only filtered sequences
// BufferAccumulator handles auto-trimming when max size exceeded
@@ -1033,7 +1035,6 @@ export class Session extends EventEmitter {
this._status = 'busy';
this._terminalBuffer.clear();
- this._outputBuffer = '';
this._textOutput.clear();
this._errorBuffer = '';
this._messages = [];
@@ -1198,7 +1199,6 @@ export class Session extends EventEmitter {
this._status = 'busy';
this._terminalBuffer.clear();
- this._outputBuffer = '';
this._textOutput.clear();
this._errorBuffer = '';
this._messages = [];
@@ -1433,7 +1433,7 @@ export class Session extends EventEmitter {
if (!data.includes('(') || !data.includes(')')) return;
// Split by newlines and process each line
- const lines = data.split(/\r?\n/);
+ const lines = data.split(NEWLINE_SPLIT_PATTERN);
for (const line of lines) {
this.parseTaskDescriptionsFromLine(line);
}
@@ -1803,8 +1803,11 @@ export class Session extends EventEmitter {
this._bashToolParser.off('toolsUpdate', this._bashToolHandlers.toolsUpdate);
this._bashToolHandlers = null;
}
- this._bashToolParser.destroy();
+ // Destroy all trackers to release memory and stop timers
+ this._bashToolParser.destroy();
+ this._taskTracker.destroy();
+ this._ralphTracker.destroy();
}
/**
@@ -1939,7 +1942,6 @@ export class Session extends EventEmitter {
this._currentTaskId = taskId;
this._status = 'busy';
this._terminalBuffer.clear();
- this._outputBuffer = '';
this._textOutput.clear();
this._errorBuffer = '';
this._messages = [];
@@ -1966,7 +1968,6 @@ export class Session extends EventEmitter {
clearBuffers(): void {
this._terminalBuffer.clear();
- this._outputBuffer = '';
this._textOutput.clear();
this._errorBuffer = '';
this._messages = [];
diff --git a/src/subagent-watcher.ts b/src/subagent-watcher.ts
index 62b1d421..2de69129 100644
--- a/src/subagent-watcher.ts
+++ b/src/subagent-watcher.ts
@@ -128,6 +128,12 @@ const STALE_IDLE_MAX_AGE_MS = 4 * 60 * 60 * 1000; // Remove idle agents older th
const STARTUP_MAX_FILE_AGE_MS = 4 * 60 * 60 * 1000; // Only load files modified in last 4 hours on startup
const MAX_TRACKED_AGENTS = 500; // Maximum agents to track (LRU eviction when exceeded)
+// Internal Claude Code agent patterns to filter out (not real user-initiated subagents)
+const INTERNAL_AGENT_PATTERNS = [
+ /^\[?SUGGESTION MODE/i, // Claude Code's internal suggestion mode
+ /^Suggest what user might/i, // Suggestion mode prompt variant
+];
+
// 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
@@ -156,6 +162,15 @@ export class SubagentWatcher extends EventEmitter {
super();
}
+ /**
+ * Check if a description matches internal Claude Code agent patterns.
+ * These are not real user-initiated subagents and should be filtered out.
+ */
+ private isInternalAgent(description: string | undefined): boolean {
+ if (!description) return false;
+ return INTERNAL_AGENT_PATTERNS.some(pattern => pattern.test(description));
+ }
+
/**
* Extract short model identifier from full model name
*/
@@ -925,6 +940,11 @@ export class SubagentWatcher extends EventEmitter {
description = this.extractDescriptionFromFile(filePath);
}
+ // Skip internal Claude Code agents (e.g., suggestion mode) - not real subagents
+ if (this.isInternalAgent(description)) {
+ return;
+ }
+
const info: SubagentInfo = {
agentId,
sessionId,
@@ -980,6 +1000,11 @@ export class SubagentWatcher extends EventEmitter {
extractedDescription = this.extractDescriptionFromFile(filePath);
}
if (extractedDescription) {
+ // Check if this is an internal agent - if so, remove it
+ if (this.isInternalAgent(extractedDescription)) {
+ this.removeAgent(agentId);
+ return;
+ }
existingInfo.description = extractedDescription;
this.emit('subagent:updated', existingInfo);
}
@@ -1094,6 +1119,11 @@ export class SubagentWatcher extends EventEmitter {
}
}
if (description) {
+ // Check if this is an internal agent - if so, remove it
+ if (this.isInternalAgent(description)) {
+ this.removeAgent(agentId);
+ return;
+ }
info.description = description;
this.emit('subagent:updated', info);
}
diff --git a/src/task-tracker.ts b/src/task-tracker.ts
index ec356b8c..3f851519 100644
--- a/src/task-tracker.ts
+++ b/src/task-tracker.ts
@@ -564,4 +564,15 @@ export class TaskTracker extends EventEmitter {
this.taskStack = [];
this.pendingToolUses.clear();
}
+
+ /**
+ * Clean up resources and release memory.
+ *
+ * Call this when the session is being destroyed to prevent memory leaks.
+ * Clears all data and removes all event listeners.
+ */
+ destroy(): void {
+ this.clear();
+ this.removeAllListeners();
+ }
}
diff --git a/src/web/public/app.js b/src/web/public/app.js
index 8c557061..69d7d1cd 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -3482,6 +3482,10 @@ class ClaudemanApp {
// Stop any existing generation first
this.stopPlanGeneration();
+ // Close old plan subagent windows and clear their state
+ // This ensures clicking on prompts shows fresh data, not cached from previous runs
+ this.closePlanSubagentWindows();
+
// Reset stopped flag to allow new SSE events
this.planGenerationStopped = false;
@@ -4795,23 +4799,181 @@ class ClaudemanApp {
return;
}
+ // Get agent info from planSubagents Map
+ const agentInfo = this.planSubagents.get(agentId);
+ const isRunning = agentInfo?.status === 'running';
+ const taskDescription = this.ralphWizardConfig?.taskDescription || 'No task description available';
+
try {
const filePath = `${agentType}/prompt.md`;
const res = await fetch(`/api/cases/${encodeURIComponent(caseName)}/ralph-wizard/file/${encodeURIComponent(filePath)}`);
const data = await res.json();
if (!data.success) {
- this.showPlanFileWindow(agentType, 'prompt.md', `Error: ${data.error}`, false);
+ // File not found - show contextual message based on agent status
+ if (isRunning) {
+ const content = this.buildAgentRunningContent(agentType, agentInfo, taskDescription);
+ this.showPlanFileWindow(agentType, 'prompt.md', content, false);
+ } else {
+ // Agent completed but no prompt file - might have failed
+ const content = this.buildAgentNoPromptContent(agentType, taskDescription);
+ this.showPlanFileWindow(agentType, 'prompt.md', content, false);
+ }
return;
}
this.showPlanFileWindow(agentType, 'prompt.md', data.data.content, false);
} catch (err) {
console.error('Failed to fetch prompt:', err);
- this.showPlanFileWindow(agentType, 'prompt.md', `Error fetching prompt: ${err.message}`, false);
+ if (isRunning) {
+ const content = this.buildAgentRunningContent(agentType, agentInfo, taskDescription);
+ this.showPlanFileWindow(agentType, 'prompt.md', content, false);
+ } else {
+ this.showPlanFileWindow(agentType, 'prompt.md', `Error fetching prompt: ${err.message}`, false);
+ }
}
}
+ buildAgentRunningContent(agentType, agentInfo, taskDescription) {
+ const typeDescriptions = {
+ research: {
+ title: 'Research Agent',
+ role: 'Gathering external resources, exploring the codebase, and identifying patterns',
+ activities: [
+ 'Reading CLAUDE.md and project documentation',
+ 'Exploring the codebase structure with Glob and Grep',
+ 'Searching the web for relevant documentation and examples',
+ 'Identifying existing patterns and conventions',
+ 'Finding similar implementations to reference',
+ ],
+ },
+ requirements: {
+ title: 'Requirements Analyst',
+ role: 'Breaking down the task into concrete, actionable requirements',
+ activities: [
+ 'Analyzing the task description for functional requirements',
+ 'Identifying non-functional requirements (performance, security)',
+ 'Detecting edge cases and error scenarios',
+ 'Defining acceptance criteria for each requirement',
+ ],
+ },
+ architecture: {
+ title: 'Architecture Planner',
+ role: 'Designing the technical approach and file structure',
+ activities: [
+ 'Identifying files to create or modify',
+ 'Planning the module/component structure',
+ 'Defining interfaces and data flow',
+ 'Ensuring consistency with existing architecture',
+ ],
+ },
+ testing: {
+ title: 'TDD Specialist',
+ role: 'Planning the test-first development strategy',
+ activities: [
+ 'Identifying test cases for each requirement',
+ 'Planning unit, integration, and E2E tests',
+ 'Defining test fixtures and mocks needed',
+ 'Ensuring edge cases have test coverage',
+ ],
+ },
+ risks: {
+ title: 'Risk Analyst',
+ role: 'Identifying potential issues and mitigation strategies',
+ activities: [
+ 'Analyzing potential failure points',
+ 'Identifying security concerns',
+ 'Flagging performance bottlenecks',
+ 'Suggesting rollback strategies',
+ ],
+ },
+ verification: {
+ title: 'Verification Expert',
+ role: 'Validating plan completeness and quality',
+ activities: [
+ 'Checking all requirements have implementation steps',
+ 'Verifying test coverage is adequate',
+ 'Ensuring dependencies are correctly ordered',
+ 'Scoring plan quality and completeness',
+ ],
+ },
+ 'execution-optimizer': {
+ title: 'Execution Optimizer',
+ role: 'Optimizing the plan for Claude Code execution',
+ activities: [
+ 'Identifying tasks that can run in parallel',
+ 'Grouping related tasks for efficiency',
+ 'Marking fresh context requirements',
+ 'Estimating token usage per task',
+ ],
+ },
+ 'final-review': {
+ title: 'Final Reviewer',
+ role: 'Performing holistic validation of the complete plan',
+ activities: [
+ 'Checking for gaps or missing steps',
+ 'Validating task ordering and dependencies',
+ 'Ensuring the plan is implementable',
+ 'Final quality assessment',
+ ],
+ },
+ };
+
+ const info = typeDescriptions[agentType] || {
+ title: agentType,
+ role: 'Processing the task',
+ activities: ['Working on the task...'],
+ };
+
+ const elapsed = agentInfo?.startTime
+ ? Math.floor((Date.now() - agentInfo.startTime) / 1000)
+ : 0;
+ const model = agentInfo?.model || 'unknown';
+
+ let content = `# ${info.title} - Still Running\n\n`;
+ content += `⏳ **Status:** Generating... (${elapsed}s elapsed)\n`;
+ content += `🤖 **Model:** ${model}\n\n`;
+ content += `---\n\n`;
+ content += `## Role\n${info.role}\n\n`;
+ content += `## Currently Working On\n`;
+ info.activities.forEach(activity => {
+ content += `- ${activity}\n`;
+ });
+ content += `\n---\n\n`;
+ content += `## Task Being Analyzed\n\n`;
+ content += `\`\`\`\n${taskDescription}\n\`\`\`\n\n`;
+ content += `---\n\n`;
+ content += `*The full prompt will be available once the agent completes.*\n`;
+ content += `*Click again after completion to see the actual prompt that was used.*`;
+
+ return content;
+ }
+
+ buildAgentNoPromptContent(agentType, taskDescription) {
+ const typeLabels = {
+ research: 'Research Agent',
+ requirements: 'Requirements Analyst',
+ architecture: 'Architecture Planner',
+ testing: 'TDD Specialist',
+ risks: 'Risk Analyst',
+ verification: 'Verification Expert',
+ 'execution-optimizer': 'Execution Optimizer',
+ 'final-review': 'Final Reviewer',
+ };
+
+ let content = `# ${typeLabels[agentType] || agentType} - No Prompt Available\n\n`;
+ content += `The prompt file for this agent was not saved.\n\n`;
+ content += `This can happen if:\n`;
+ content += `- The agent failed before completing\n`;
+ content += `- The plan generation was cancelled\n`;
+ content += `- There was an error saving the output\n\n`;
+ content += `---\n\n`;
+ content += `## Task Description\n\n`;
+ content += `\`\`\`\n${taskDescription}\n\`\`\``;
+
+ return content;
+ }
+
async showPlanAgentFiles(agentId, agentType) {
const caseName = this.ralphWizardConfig?.caseName;
if (!caseName) {
diff --git a/src/web/public/index.html b/src/web/public/index.html
index 87b2d534..15b04f61 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -426,15 +426,19 @@
-
+
Not active
+
+
+
+
-
+
@@ -450,92 +454,81 @@
- How long to run the respawn loop
+
Prompts
-
-
-
-
-
-
-
-
Sent only when /init completes but Claude stays idle
-
-
-
-
-
- Presses Enter to accept plan approvals and default question options when Claude is waiting
-
-
-
-
-
+
Behavior
+
+
+
+
+ Auto-accept presses Enter for plan approvals and default question options
-
-
-
-
-
-
- tokens
+
Token Management
+
+
+
+
+
+
+ tokens
+
+
+ Summarizes context while preserving key information
-
- Summarizes context while preserving key information
-
-
-
-
-
-
-
- tokens
+
+
+
+
+
+ tokens
+
+ Full context reset at threshold (use higher than compact)
- Completely resets context (use higher threshold than compact)
+
File Watching
- Auto-popup new images (screenshots, etc.) in this session's directory
+ Auto-popup new images (screenshots, generated files) in this session's directory
@@ -547,34 +540,34 @@
- Enable Ralph / Todo tracker for this session (auto-detects loops and todos)
+ Auto-detects loops and todos in Claude output
- The phrase Claude outputs when the loop is complete (without <promise> tags)
+ Phrase Claude outputs when loop is complete (without <promise> tags)
-
-
-
- Maximum respawn cycles (0 = unlimited)
-
-
-
-
-
- Maximum todos to track per session
-
-
-
-
-
-
- min
+
Limits
+
+
+
+
+ 0 = unlimited
+
+
+
+
+ Per session
+
+
+
+
+
+ min
+
- Minutes before todos auto-expire
@@ -1153,14 +1146,6 @@
Advanced Options
-
-
-
- Automatically start the loop when plan generation completes
-