chore: bump version to 0.1434

This commit is contained in:
arkon
2026-01-30 11:30:51 +01:00
parent da5d86a84f
commit 7dfd97df58
15 changed files with 1300 additions and 122 deletions
+24 -1
View File
@@ -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 `<promise>COMPLETION_PHRASE</promise>` 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
+83
View File
@@ -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); });
+1 -1
View File
@@ -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",
+691
View File
@@ -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 <number> 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 "$@"
+24 -8
View File
@@ -145,8 +145,11 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
private _workingDir: string;
private _homeDir: string;
// Track normalized paths to detect equivalents (normalized -> original)
private _normalizedPathMap: Map<string, string> = new Map();
// Track auto-remove timers for cleanup
private _autoRemoveTimers: Set<ReturnType<typeof setTimeout>> = 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<BashToolParserEvents> {
*/
reset(): void {
this._activeTools.clear();
this._normalizedPathMap.clear();
this._lineBuffer = '';
this._lastToolId = null;
this.emitUpdate();
@@ -408,7 +410,7 @@ export class BashToolParser extends EventEmitter<BashToolParserEvents> {
* @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<BashToolParserEvents> {
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<BashToolParserEvents> {
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<BashToolParserEvents> {
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<BashToolParserEvents> {
* 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();
}
}
+4
View File
@@ -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 ==========
+22 -6
View File
@@ -67,9 +67,12 @@ export class ImageWatcher extends EventEmitter {
/** Map of sessionId -> working directory path */
private sessionDirs = new Map<string, string>();
/** Debounce timers for rapid image creation */
/** Debounce timers for rapid image creation (keyed by filePath) */
private debounceTimers = new Map<string, NodeJS.Timeout>();
/** Track which session owns each debounce timer (for cleanup) */
private timerToSession = new Map<string, string>();
/** 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);
}
/**
+18
View File
@@ -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();
}
}
+15 -14
View File
@@ -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 = [];
+30
View File
@@ -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);
}
+11
View File
@@ -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();
}
}
+164 -2
View File
@@ -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) {
+69 -84
View File
@@ -426,15 +426,19 @@
<div class="modal-tab-content" id="respawn-tab">
<!-- Respawn Settings Section -->
<div class="session-respawn-section" id="sessionRespawnSection">
<div class="section-divider">
<div class="respawn-header">
<div class="session-respawn-status" id="sessionRespawnStatus">
<span class="respawn-status-indicator"></span>
<span class="respawn-status-text">Not active</span>
</div>
<div class="respawn-actions">
<button class="btn-toolbar btn-success btn-sm" onclick="app.enableRespawnFromModal()" id="modalEnableRespawnBtn">Enable</button>
<button class="btn-toolbar btn-danger btn-sm" onclick="app.stopRespawnFromModal()" id="modalStopRespawnBtn" style="display: none;">Stop</button>
</div>
</div>
<div class="form-row">
<label>Run Duration</label>
<label>Duration</label>
<div class="duration-presets">
<button type="button" class="duration-preset-btn" data-minutes="30" onclick="app.selectDurationPreset('30')">30m</button>
<button type="button" class="duration-preset-btn" data-minutes="60" onclick="app.selectDurationPreset('60')">1h</button>
@@ -450,92 +454,81 @@
</div>
</div>
</div>
<span class="form-hint">How long to run the respawn loop</span>
</div>
<div class="form-section-header">Prompts</div>
<div class="form-row">
<label>Update Prompt</label>
<input type="text" id="modalRespawnPrompt" value="update all the docs and CLAUDE.md" placeholder="Prompt to send when idle" onchange="app.autoSaveRespawnConfig()">
</div>
<div class="form-row">
<label>After Update Prompt</label>
<div class="checkbox-group-inline">
<label class="checkbox-inline">
<input type="checkbox" id="modalRespawnSendClear" checked onchange="app.autoSaveRespawnConfig()">
<span>Send /clear</span>
</label>
<label class="checkbox-inline">
<input type="checkbox" id="modalRespawnSendInit" checked onchange="app.autoSaveRespawnConfig()">
<span>Send /init</span>
</label>
</div>
</div>
<div class="form-row">
<label>Kickstart Prompt</label>
<input type="text" id="modalRespawnKickstart" value="" placeholder="Optional: prompt if /init doesn't trigger work" onchange="app.autoSaveRespawnConfig()">
<span class="form-hint">Sent only when /init completes but Claude stays idle</span>
</div>
<div class="form-row">
<label>Auto-Accept Plan Mode</label>
<div class="checkbox-group-inline">
<label class="checkbox-inline">
<input type="checkbox" id="modalRespawnAutoAccept" checked onchange="app.autoSaveRespawnConfig()">
<span>Accept plan mode approvals</span>
</label>
</div>
<span class="form-hint">Presses Enter to accept plan approvals and default question options when Claude is waiting</span>
</div>
<div class="respawn-actions">
<button class="btn-toolbar btn-success" onclick="app.enableRespawnFromModal()" id="modalEnableRespawnBtn">Enable Respawn</button>
<button class="btn-toolbar btn-danger" onclick="app.stopRespawnFromModal()" id="modalStopRespawnBtn" style="display: none;">Stop Respawn</button>
<div class="form-section-header">Behavior</div>
<div class="respawn-options-row">
<label class="checkbox-inline">
<input type="checkbox" id="modalRespawnSendClear" checked onchange="app.autoSaveRespawnConfig()">
<span>Send /clear</span>
</label>
<label class="checkbox-inline">
<input type="checkbox" id="modalRespawnSendInit" checked onchange="app.autoSaveRespawnConfig()">
<span>Send /init</span>
</label>
<label class="checkbox-inline">
<input type="checkbox" id="modalRespawnAutoAccept" checked onchange="app.autoSaveRespawnConfig()">
<span>Auto-accept prompts</span>
</label>
</div>
<span class="form-hint">Auto-accept presses Enter for plan approvals and default question options</span>
</div>
</div><!-- End respawn-tab -->
<!-- Context Management Tab Content -->
<div class="modal-tab-content hidden" id="context-tab">
<div class="form-row">
<label>Auto-Compact Context</label>
<div class="auto-clear-row">
<label class="checkbox-inline">
<input type="checkbox" id="modalAutoCompactEnabled" onchange="app.autoSaveAutoCompact()">
<span>Enable at</span>
</label>
<div class="input-suffix input-suffix-sm">
<input type="number" id="modalAutoCompactThreshold" value="110000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoCompact()">
<span>tokens</span>
<div class="form-section-header">Token Management</div>
<div class="context-settings-grid">
<div class="context-setting">
<div class="context-setting-header">
<label class="checkbox-inline">
<input type="checkbox" id="modalAutoCompactEnabled" onchange="app.autoSaveAutoCompact()">
<span>Auto-Compact</span>
</label>
<div class="input-suffix input-suffix-sm">
<input type="number" id="modalAutoCompactThreshold" value="110000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoCompact()">
<span>tokens</span>
</div>
</div>
<input type="text" id="modalAutoCompactPrompt" value="" placeholder="Focus prompt (optional)" onchange="app.autoSaveAutoCompact()">
<span class="form-hint">Summarizes context while preserving key information</span>
</div>
<input type="text" id="modalAutoCompactPrompt" value="" placeholder="Optional: focus prompt for compact" class="mt-sm" onchange="app.autoSaveAutoCompact()">
<span class="form-hint">Summarizes context while preserving key information</span>
</div>
<div class="form-row">
<label>Auto-Clear Context</label>
<div class="auto-clear-row">
<label class="checkbox-inline">
<input type="checkbox" id="modalAutoClearEnabled" onchange="app.autoSaveAutoClear()">
<span>Enable at</span>
</label>
<div class="input-suffix input-suffix-sm">
<input type="number" id="modalAutoClearThreshold" value="140000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoClear()">
<span>tokens</span>
<div class="context-setting">
<div class="context-setting-header">
<label class="checkbox-inline">
<input type="checkbox" id="modalAutoClearEnabled" onchange="app.autoSaveAutoClear()">
<span>Auto-Clear</span>
</label>
<div class="input-suffix input-suffix-sm">
<input type="number" id="modalAutoClearThreshold" value="140000" min="10000" max="500000" step="10000" onchange="app.autoSaveAutoClear()">
<span>tokens</span>
</div>
</div>
<span class="form-hint">Full context reset at threshold (use higher than compact)</span>
</div>
<span class="form-hint">Completely resets context (use higher threshold than compact)</span>
</div>
<div class="form-section-header">File Watching</div>
<div class="form-row form-row-switch">
<label>Image Watcher</label>
<label class="switch">
<input type="checkbox" id="modalImageWatcherEnabled" checked onchange="app.toggleSessionImageWatcher()">
<span class="slider"></span>
</label>
<span class="form-hint">Auto-popup new images (screenshots, etc.) in this session's directory</span>
<span class="form-hint">Auto-popup new images (screenshots, generated files) in this session's directory</span>
</div>
</div><!-- End context-tab -->
@@ -547,34 +540,34 @@
<input type="checkbox" id="modalRalphEnabled">
<span class="slider"></span>
</label>
<span class="form-hint">Enable Ralph / Todo tracker for this session (auto-detects loops and todos)</span>
<span class="form-hint">Auto-detects loops and todos in Claude output</span>
</div>
<div class="form-row">
<label>Completion Phrase</label>
<input type="text" id="modalRalphPhrase" placeholder="e.g., COMPLETE, DONE, FINISHED">
<span class="form-hint">The phrase Claude outputs when the loop is complete (without &lt;promise&gt; tags)</span>
<span class="form-hint">Phrase Claude outputs when loop is complete (without &lt;promise&gt; tags)</span>
</div>
<div class="form-row">
<label>Max Iterations</label>
<input type="number" id="modalRalphMaxIterations" value="0" min="0" max="1000">
<span class="form-hint">Maximum respawn cycles (0 = unlimited)</span>
</div>
<div class="form-row">
<label>Max Todos</label>
<input type="number" id="modalRalphMaxTodos" value="50" min="1" max="500">
<span class="form-hint">Maximum todos to track per session</span>
</div>
<div class="form-row">
<label>Todo Expiration</label>
<div class="input-suffix input-suffix-sm">
<input type="number" id="modalRalphTodoExpiration" value="60" min="1" max="1440">
<span>min</span>
<div class="form-section-header">Limits</div>
<div class="ralph-limits-grid">
<div class="form-col">
<label>Max Iterations</label>
<input type="number" id="modalRalphMaxIterations" value="0" min="0" max="1000">
<span class="form-hint">0 = unlimited</span>
</div>
<div class="form-col">
<label>Max Todos</label>
<input type="number" id="modalRalphMaxTodos" value="50" min="1" max="500">
<span class="form-hint">Per session</span>
</div>
<div class="form-col">
<label>Todo Expiration</label>
<div class="input-suffix input-suffix-sm">
<input type="number" id="modalRalphTodoExpiration" value="60" min="1" max="1440">
<span>min</span>
</div>
</div>
<span class="form-hint">Minutes before todos auto-expire</span>
</div>
<div class="ralph-config-actions">
@@ -1153,14 +1146,6 @@
<details class="advanced-options">
<summary>Advanced Options</summary>
<div class="advanced-options-content">
<div class="form-row form-row-switch">
<label>Auto-Start After Plan</label>
<label class="switch">
<input type="checkbox" id="ralphAutoStart">
<span class="slider"></span>
</label>
<span class="form-hint">Automatically start the loop when plan generation completes</span>
</div>
<div class="form-row form-row-switch">
<label>Enable Respawn</label>
<label class="switch">
+125 -4
View File
@@ -1754,6 +1754,8 @@ body {
.btn-toolbar.btn-claude svg {
flex-shrink: 0;
width: 12px;
height: 12px;
}
/* Shell Button - Special styling */
@@ -2492,6 +2494,21 @@ body {
overflow: hidden;
}
/* Fix CSS specificity: ID selector overrides class selector, so we need this explicit rule */
#summary-tab.hidden,
#respawn-tab.hidden,
#context-tab.hidden,
#ralph-tab.hidden {
display: none !important;
}
/* Also hide child elements when parent tab is hidden (belt and suspenders) */
#summary-tab.hidden .run-summary-filters,
#summary-tab.hidden .run-summary-timeline,
#summary-tab.hidden .run-summary-footer {
display: none !important;
}
#summary-tab .run-summary-filters {
flex-shrink: 0;
}
@@ -2958,13 +2975,35 @@ body {
width: 60px;
}
/* Respawn header with status and actions */
.respawn-header {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border);
}
.respawn-header .respawn-actions {
margin: 0;
padding: 0;
border: none;
}
/* Respawn options row - inline checkboxes */
.respawn-options-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 0.25rem;
}
/* Respawn actions in modal */
.respawn-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--border);
}
.session-respawn-actions {
@@ -3003,7 +3042,89 @@ body {
}
.modal-tab-content.hidden {
display: none;
display: none !important;
}
/* Context Tab Settings Grid */
.context-settings-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.context-setting {
padding: 0.875rem;
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border);
border-radius: 6px;
}
.context-setting-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.625rem;
}
.context-setting-header .checkbox-inline span {
font-weight: 500;
}
.context-setting-header .input-suffix-sm {
flex-shrink: 0;
}
.context-setting-header .input-suffix-sm input {
width: 70px;
}
.context-setting input[type="text"] {
width: 100%;
padding: 0.375rem 0.5rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.75rem;
}
.context-setting input[type="text"]:focus {
outline: none;
border-color: var(--accent);
}
.context-setting .form-hint {
margin-top: 0.5rem;
}
/* Ralph Limits Grid */
.ralph-limits-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 0.5rem;
}
@media (max-width: 500px) {
.ralph-limits-grid {
grid-template-columns: 1fr 1fr;
}
}
.ralph-limits-grid .form-col input[type="number"] {
width: 100%;
padding: 0.375rem 0.5rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.8rem;
}
.ralph-limits-grid .form-col .form-hint {
font-size: 0.6rem;
}
/* Ralph Config Actions */
+19 -2
View File
@@ -14,7 +14,7 @@ import Fastify, { FastifyInstance, FastifyReply } from 'fastify';
import fastifyStatic from '@fastify/static';
import path, { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs';
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, rmSync } from 'node:fs';
import fs from 'node:fs/promises';
import { execSync } from 'node:child_process';
import { homedir, totalmem, freemem, loadavg, cpus } from 'node:os';
@@ -2518,6 +2518,17 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
const resolvedBase = resolve(casesDir);
if (resolvedCase.startsWith(resolvedBase) && existsSync(casePath)) {
outputDir = join(casePath, 'ralph-wizard');
// Clear old ralph-wizard directory to ensure fresh prompts for each generation
// This prevents stale prompts from previous runs being shown when clicking on agents
if (existsSync(outputDir)) {
try {
rmSync(outputDir, { recursive: true, force: true });
console.log(`[API] Cleared old ralph-wizard directory: ${outputDir}`);
} catch (err) {
console.warn(`[API] Failed to clear ralph-wizard directory:`, err);
}
}
}
}
@@ -2665,11 +2676,17 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
});
// Read a specific ralph-wizard file
this.app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req) => {
// Cache disabled to ensure fresh prompts when starting new plan generations
this.app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req, reply) => {
const { caseName, filePath } = req.params as { caseName: string; filePath: string };
const casesDir = join(homedir(), 'claudeman-cases');
const casePath = join(casesDir, caseName);
// Prevent browser caching - prompts change between plan generations
reply.header('Cache-Control', 'no-store, no-cache, must-revalidate');
reply.header('Pragma', 'no-cache');
reply.header('Expires', '0');
// Security: Path traversal protection for case name
const resolvedCase = resolve(casePath);
const resolvedBase = resolve(casesDir);