mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
chore: bump version to 0.1434
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Executable
+691
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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 = [];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 <promise> tags)</span>
|
||||
<span class="form-hint">Phrase Claude outputs when loop is complete (without <promise> 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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user