mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
feat: implement DEC mode 2026 synchronized output to reduce terminal flicker
- Server wraps SSE terminal batches with DEC 2026 sync markers - Client extracts and strips markers, writes atomically via RAF batching - Native terminals (WezTerm, Kitty, etc.) get full sync support - xterm.js web gets application-layer sync via marker extraction Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ When user says "COM": 1) Increment version in BOTH `package.json` AND `CLAUDE.md
|
|||||||
|
|
||||||
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
|
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
|
||||||
|
|
||||||
**Version**: 0.1367 (must match `package.json`)
|
**Version**: 0.1368 (must match `package.json`)
|
||||||
|
|
||||||
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
|
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"name": "claudeman",
|
||||||
"version": "0.1367",
|
"version": "0.1368",
|
||||||
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Suggested Fixes for Respawn Controller Issues
|
||||||
|
|
||||||
|
**DO NOT EXECUTE** - Waiting for user review
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
|
||||||
|
### Fix for Issue #1
|
||||||
|
**Enable respawn controller**
|
||||||
|
|
||||||
|
Via API:
|
||||||
|
```bash
|
||||||
|
curl -sk -X POST 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/respawn/enable'
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via web UI: Open session tab -> Respawn panel -> Enable
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Respawn Controller Issues - w3-reddit-analyse
|
||||||
|
|
||||||
|
Monitoring started: Sun Jan 25 09:40:47 PM CET 2026
|
||||||
|
Session ID: 5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98
|
||||||
|
|
||||||
|
## Issues Detected
|
||||||
|
|
||||||
|
|
||||||
|
### Issue #1 [INFO] - 2026-01-25 21:40:48
|
||||||
|
**Respawn controller not enabled**
|
||||||
|
|
||||||
|
Respawn is currently disabled for this session. State: null
|
||||||
|
|
||||||
Executable
+313
@@ -0,0 +1,313 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Respawn Controller Monitor for w3-reddit-analyse
|
||||||
|
# Monitors for 12 hours, logs issues and suggested fixes
|
||||||
|
|
||||||
|
SESSION_ID="5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98"
|
||||||
|
SESSION_NAME="w3-reddit-analyse"
|
||||||
|
ISSUES_FILE="/home/arkon/default/claudeman/respawn-issues.md"
|
||||||
|
FIXES_FILE="/home/arkon/default/claudeman/respawn-fixes.md"
|
||||||
|
LOG_FILE="/home/arkon/default/claudeman/respawn-monitor.log"
|
||||||
|
API_BASE="https://localhost:3000"
|
||||||
|
DURATION_HOURS=12
|
||||||
|
POLL_INTERVAL=30 # seconds
|
||||||
|
|
||||||
|
# Initialize files
|
||||||
|
cat > "$ISSUES_FILE" << 'EOF'
|
||||||
|
# Respawn Controller Issues - w3-reddit-analyse
|
||||||
|
|
||||||
|
Monitoring started: $(date)
|
||||||
|
Session ID: 5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98
|
||||||
|
|
||||||
|
## Issues Detected
|
||||||
|
|
||||||
|
EOF
|
||||||
|
sed -i "s/\$(date)/$(date)/" "$ISSUES_FILE"
|
||||||
|
|
||||||
|
cat > "$FIXES_FILE" << 'EOF'
|
||||||
|
# Suggested Fixes for Respawn Controller Issues
|
||||||
|
|
||||||
|
**DO NOT EXECUTE** - Waiting for user review
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "=== Respawn Monitor Started: $(date) ===" > "$LOG_FILE"
|
||||||
|
echo "Session: $SESSION_NAME ($SESSION_ID)" >> "$LOG_FILE"
|
||||||
|
echo "Duration: $DURATION_HOURS hours" >> "$LOG_FILE"
|
||||||
|
echo "Poll interval: ${POLL_INTERVAL}s" >> "$LOG_FILE"
|
||||||
|
echo "" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Track state for anomaly detection
|
||||||
|
PREV_STATE=""
|
||||||
|
PREV_TOKENS=0
|
||||||
|
STATE_STUCK_COUNT=0
|
||||||
|
LAST_ACTIVITY_TIME=$(date +%s)
|
||||||
|
ISSUE_COUNT=0
|
||||||
|
|
||||||
|
log_issue() {
|
||||||
|
local severity="$1"
|
||||||
|
local title="$2"
|
||||||
|
local details="$3"
|
||||||
|
ISSUE_COUNT=$((ISSUE_COUNT + 1))
|
||||||
|
|
||||||
|
echo "" >> "$ISSUES_FILE"
|
||||||
|
echo "### Issue #$ISSUE_COUNT [$severity] - $(date '+%Y-%m-%d %H:%M:%S')" >> "$ISSUES_FILE"
|
||||||
|
echo "**$title**" >> "$ISSUES_FILE"
|
||||||
|
echo "" >> "$ISSUES_FILE"
|
||||||
|
echo "$details" >> "$ISSUES_FILE"
|
||||||
|
echo "" >> "$ISSUES_FILE"
|
||||||
|
|
||||||
|
echo "[$(date '+%H:%M:%S')] ISSUE #$ISSUE_COUNT [$severity]: $title" >> "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_fix() {
|
||||||
|
local issue_num="$1"
|
||||||
|
local fix_title="$2"
|
||||||
|
local fix_details="$3"
|
||||||
|
|
||||||
|
echo "" >> "$FIXES_FILE"
|
||||||
|
echo "### Fix for Issue #$issue_num" >> "$FIXES_FILE"
|
||||||
|
echo "**$fix_title**" >> "$FIXES_FILE"
|
||||||
|
echo "" >> "$FIXES_FILE"
|
||||||
|
echo "$fix_details" >> "$FIXES_FILE"
|
||||||
|
echo "" >> "$FIXES_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
END_TIME=$(($(date +%s) + DURATION_HOURS * 3600))
|
||||||
|
|
||||||
|
while [ $(date +%s) -lt $END_TIME ]; do
|
||||||
|
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# Fetch session status
|
||||||
|
RESPONSE=$(curl -sk "$API_BASE/api/sessions/$SESSION_ID" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$RESPONSE" ] || [ "$RESPONSE" = "null" ]; then
|
||||||
|
log_issue "CRITICAL" "API request failed or session not found" "Could not fetch session data. Server may be down or session deleted."
|
||||||
|
log_fix "$ISSUE_COUNT" "Check server status" "1. Check if claudeman-web is running: \`systemctl --user status claudeman-web\`
|
||||||
|
2. If down, restart: \`systemctl --user restart claudeman-web\`
|
||||||
|
3. Check server logs: \`journalctl --user -u claudeman-web -n 50\`"
|
||||||
|
sleep $POLL_INTERVAL
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse response
|
||||||
|
STATUS=$(echo "$RESPONSE" | jq -r '.status // "unknown"')
|
||||||
|
RESPAWN_ENABLED=$(echo "$RESPONSE" | jq -r '.respawnEnabled // "null"')
|
||||||
|
RESPAWN_STATE=$(echo "$RESPONSE" | jq -r '.respawnState // "null"')
|
||||||
|
INPUT_TOKENS=$(echo "$RESPONSE" | jq -r '.inputTokens // 0')
|
||||||
|
OUTPUT_TOKENS=$(echo "$RESPONSE" | jq -r '.outputTokens // 0')
|
||||||
|
TOTAL_TOKENS=$((INPUT_TOKENS + OUTPUT_TOKENS))
|
||||||
|
|
||||||
|
# Fetch respawn controller details if enabled
|
||||||
|
if [ "$RESPAWN_ENABLED" = "true" ]; then
|
||||||
|
RESPAWN_DETAILS=$(curl -sk "$API_BASE/api/sessions/$SESSION_ID/respawn/status" 2>/dev/null)
|
||||||
|
RESPAWN_STATE=$(echo "$RESPAWN_DETAILS" | jq -r '.state // "unknown"')
|
||||||
|
DETECTION_STATUS=$(echo "$RESPAWN_DETAILS" | jq -r '.detectionStatus // {}')
|
||||||
|
IDLE_CONFIDENCE=$(echo "$DETECTION_STATUS" | jq -r '.idleConfidence // 0')
|
||||||
|
TIME_IN_STATE=$(echo "$RESPAWN_DETAILS" | jq -r '.timeInState // 0')
|
||||||
|
LAST_ERROR=$(echo "$RESPAWN_DETAILS" | jq -r '.lastError // "none"')
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[$TIMESTAMP] status=$STATUS respawn=$RESPAWN_ENABLED state=$RESPAWN_STATE tokens=$TOTAL_TOKENS" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# Check 1: Respawn not enabled when it should be
|
||||||
|
if [ "$RESPAWN_ENABLED" = "null" ] || [ "$RESPAWN_ENABLED" = "false" ]; then
|
||||||
|
if [ -z "$RESPAWN_NOT_ENABLED_LOGGED" ]; then
|
||||||
|
log_issue "INFO" "Respawn controller not enabled" "Respawn is currently disabled for this session. State: $RESPAWN_ENABLED"
|
||||||
|
log_fix "$ISSUE_COUNT" "Enable respawn controller" "Via API:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/respawn/enable'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Or via web UI: Open session tab -> Respawn panel -> Enable"
|
||||||
|
RESPAWN_NOT_ENABLED_LOGGED=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
unset RESPAWN_NOT_ENABLED_LOGGED
|
||||||
|
|
||||||
|
# Check 2: State stuck for too long
|
||||||
|
if [ "$RESPAWN_STATE" = "$PREV_STATE" ] && [ -n "$PREV_STATE" ]; then
|
||||||
|
STATE_STUCK_COUNT=$((STATE_STUCK_COUNT + 1))
|
||||||
|
|
||||||
|
# Stuck in same state for 10+ minutes (20 polls at 30s)
|
||||||
|
if [ $STATE_STUCK_COUNT -ge 20 ]; then
|
||||||
|
STUCK_MINS=$((STATE_STUCK_COUNT * POLL_INTERVAL / 60))
|
||||||
|
log_issue "WARNING" "State stuck: $RESPAWN_STATE for ${STUCK_MINS}+ minutes" "The respawn controller has been in '$RESPAWN_STATE' state for over $STUCK_MINS minutes.
|
||||||
|
Time in state: ${TIME_IN_STATE}ms
|
||||||
|
Detection status: $DETECTION_STATUS"
|
||||||
|
|
||||||
|
case "$RESPAWN_STATE" in
|
||||||
|
"watching")
|
||||||
|
log_fix "$ISSUE_COUNT" "Watching state stuck" "The controller is watching but not detecting idle. Possible causes:
|
||||||
|
1. Session is actively working (expected behavior)
|
||||||
|
2. Idle detection thresholds too high
|
||||||
|
|
||||||
|
Check session activity:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk '$API_BASE/api/sessions/$SESSION_ID' | jq '{status, lastActivityAt}'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
If truly idle, try adjusting config:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/respawn/config' -H 'Content-Type: application/json' -d '{\"idleThresholdMs\": 30000}'
|
||||||
|
\`\`\`"
|
||||||
|
;;
|
||||||
|
"confirming_idle")
|
||||||
|
log_fix "$ISSUE_COUNT" "Confirming idle stuck" "Stuck confirming idle state. The session may be oscillating between idle/working.
|
||||||
|
|
||||||
|
Check idle confidence:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk '$API_BASE/api/sessions/$SESSION_ID/respawn/status' | jq '.detectionStatus'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Consider increasing confirmation time or check for output patterns that reset detection."
|
||||||
|
;;
|
||||||
|
"ai_checking")
|
||||||
|
log_fix "$ISSUE_COUNT" "AI check stuck" "AI idle check is taking too long or failing.
|
||||||
|
|
||||||
|
Check AI checker status and logs:
|
||||||
|
\`\`\`bash
|
||||||
|
journalctl --user -u claudeman-web -n 100 | grep -i 'ai.*check\\|idle.*check'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
The AI checker spawns a separate Claude process. It may be hanging. Consider:
|
||||||
|
1. Check for stuck Claude processes: \`ps aux | grep claude\`
|
||||||
|
2. Restart the respawn controller:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/respawn/stop'
|
||||||
|
sleep 2
|
||||||
|
curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/respawn/start'
|
||||||
|
\`\`\`"
|
||||||
|
;;
|
||||||
|
"sending_"*|"waiting_"*)
|
||||||
|
log_fix "$ISSUE_COUNT" "Send/wait state stuck" "Stuck in $RESPAWN_STATE - the controller is trying to send a command but may not be receiving expected response.
|
||||||
|
|
||||||
|
Check terminal output:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk '$API_BASE/api/sessions/$SESSION_ID/terminal?tail=10000' | tail -100
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Try restarting respawn:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/respawn/stop'
|
||||||
|
curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/respawn/start'
|
||||||
|
\`\`\`"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_fix "$ISSUE_COUNT" "Unknown stuck state" "State '$RESPAWN_STATE' is stuck. Review respawn-controller.ts for this state's expected behavior and transitions."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
STATE_STUCK_COUNT=0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
STATE_STUCK_COUNT=0
|
||||||
|
PREV_STATE="$RESPAWN_STATE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 3: Error in respawn controller
|
||||||
|
if [ "$LAST_ERROR" != "none" ] && [ "$LAST_ERROR" != "null" ] && [ -n "$LAST_ERROR" ]; then
|
||||||
|
log_issue "ERROR" "Respawn controller error" "Error reported: $LAST_ERROR"
|
||||||
|
log_fix "$ISSUE_COUNT" "Resolve respawn error" "Error: $LAST_ERROR
|
||||||
|
|
||||||
|
General recovery steps:
|
||||||
|
1. Stop respawn: \`curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/respawn/stop'\`
|
||||||
|
2. Check session health: \`curl -sk '$API_BASE/api/sessions/$SESSION_ID' | jq .\`
|
||||||
|
3. Restart respawn: \`curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/respawn/start'\`
|
||||||
|
|
||||||
|
If error persists, check server logs:
|
||||||
|
\`journalctl --user -u claudeman-web -n 200 | grep -i error\`"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 4: Token count not progressing during active work
|
||||||
|
if [ "$STATUS" = "busy" ] && [ "$RESPAWN_STATE" = "watching" ]; then
|
||||||
|
if [ $TOTAL_TOKENS -eq $PREV_TOKENS ] && [ $PREV_TOKENS -gt 0 ]; then
|
||||||
|
TOKENS_STUCK_COUNT=$((TOKENS_STUCK_COUNT + 1))
|
||||||
|
if [ $TOKENS_STUCK_COUNT -ge 10 ]; then # 5 minutes
|
||||||
|
log_issue "WARNING" "Tokens not increasing while busy" "Session status is 'busy' but token count hasn't changed in 5+ minutes.
|
||||||
|
Current: $TOTAL_TOKENS, Previous: $PREV_TOKENS
|
||||||
|
This could indicate a hung Claude process."
|
||||||
|
log_fix "$ISSUE_COUNT" "Check for hung session" "The session may be hung. Check:
|
||||||
|
|
||||||
|
1. Session process status:
|
||||||
|
\`\`\`bash
|
||||||
|
screen -ls | grep reddit
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. Attach to screen and check:
|
||||||
|
\`\`\`bash
|
||||||
|
screen -r claudeman-w3-reddit-analyse
|
||||||
|
# Press Ctrl+A D to detach
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
3. If hung, restart the session via web UI or:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/stop'
|
||||||
|
# Then start a new session
|
||||||
|
\`\`\`"
|
||||||
|
TOKENS_STUCK_COUNT=0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TOKENS_STUCK_COUNT=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
PREV_TOKENS=$TOTAL_TOKENS
|
||||||
|
|
||||||
|
# Check 5: Rapid state cycling (flapping)
|
||||||
|
if [ "$RESPAWN_STATE" != "$PREV_STATE" ]; then
|
||||||
|
STATE_CHANGES=$((STATE_CHANGES + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check every 5 minutes for flapping
|
||||||
|
if [ $((STATE_CHANGES_CHECK_COUNT % 10)) -eq 0 ] && [ $STATE_CHANGES_CHECK_COUNT -gt 0 ]; then
|
||||||
|
if [ $STATE_CHANGES -gt 20 ]; then # More than 20 state changes in 5 mins
|
||||||
|
log_issue "WARNING" "State flapping detected" "Respawn controller changed states $STATE_CHANGES times in the last 5 minutes. This indicates instability."
|
||||||
|
log_fix "$ISSUE_COUNT" "Reduce state flapping" "The controller is cycling states too rapidly. This usually means:
|
||||||
|
1. Idle detection is too sensitive
|
||||||
|
2. Session output is oscillating
|
||||||
|
|
||||||
|
Adjust detection thresholds:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/respawn/config' -H 'Content-Type: application/json' -d '{
|
||||||
|
\"idleThresholdMs\": 60000,
|
||||||
|
\"completionConfirmMs\": 15000
|
||||||
|
}'
|
||||||
|
\`\`\`"
|
||||||
|
fi
|
||||||
|
STATE_CHANGES=0
|
||||||
|
fi
|
||||||
|
STATE_CHANGES_CHECK_COUNT=$((STATE_CHANGES_CHECK_COUNT + 1))
|
||||||
|
|
||||||
|
# Check 6: High token usage without respawn cycling
|
||||||
|
if [ $TOTAL_TOKENS -gt 180000 ]; then
|
||||||
|
log_issue "WARNING" "High token usage: $TOTAL_TOKENS" "Token count is very high. Auto-compact or respawn should have triggered by now."
|
||||||
|
log_fix "$ISSUE_COUNT" "Handle high token usage" "Tokens at $TOTAL_TOKENS. Consider:
|
||||||
|
|
||||||
|
1. Manual compact:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk -X POST '$API_BASE/api/sessions/$SESSION_ID/input' -H 'Content-Type: application/json' -d '{\"input\": \"/compact\\r\"}'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. Check auto-compact settings:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sk '$API_BASE/api/sessions/$SESSION_ID' | jq '{autoCompactEnabled, autoCompactThreshold}'
|
||||||
|
\`\`\`"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep $POLL_INTERVAL
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> "$ISSUES_FILE"
|
||||||
|
echo "---" >> "$ISSUES_FILE"
|
||||||
|
echo "Monitoring completed: $(date)" >> "$ISSUES_FILE"
|
||||||
|
echo "Total issues detected: $ISSUE_COUNT" >> "$ISSUES_FILE"
|
||||||
|
|
||||||
|
echo "" >> "$FIXES_FILE"
|
||||||
|
echo "---" >> "$FIXES_FILE"
|
||||||
|
echo "Monitoring completed: $(date)" >> "$FIXES_FILE"
|
||||||
|
|
||||||
|
echo "" >> "$LOG_FILE"
|
||||||
|
echo "=== Monitoring Complete: $(date) ===" >> "$LOG_FILE"
|
||||||
|
echo "Total issues: $ISSUE_COUNT" >> "$LOG_FILE"
|
||||||
+159
-16
@@ -6,10 +6,63 @@ const DEFAULT_SCROLLBACK = 5000;
|
|||||||
// Wrap terminal writes with these markers to prevent partial-frame flicker.
|
// Wrap terminal writes with these markers to prevent partial-frame flicker.
|
||||||
// Terminal buffers all output between markers and renders atomically.
|
// Terminal buffers all output between markers and renders atomically.
|
||||||
// Supported by: WezTerm, Kitty, Ghostty, iTerm2 3.5+, Windows Terminal, VSCode terminal
|
// Supported by: WezTerm, Kitty, Ghostty, iTerm2 3.5+, Windows Terminal, VSCode terminal
|
||||||
// Unsupported terminals ignore these sequences harmlessly.
|
// xterm.js doesn't support DEC 2026 natively, so we implement buffering ourselves.
|
||||||
const DEC_SYNC_START = '\x1b[?2026h';
|
const DEC_SYNC_START = '\x1b[?2026h';
|
||||||
const DEC_SYNC_END = '\x1b[?2026l';
|
const DEC_SYNC_END = '\x1b[?2026l';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process data containing DEC 2026 sync markers.
|
||||||
|
* Strips markers and returns segments that should be written atomically.
|
||||||
|
* Each returned segment represents content between SYNC_START and SYNC_END.
|
||||||
|
* Content outside sync blocks is returned as-is.
|
||||||
|
*
|
||||||
|
* @param {string} data - Raw terminal data with potential sync markers
|
||||||
|
* @returns {string[]} - Array of content segments to write (markers stripped)
|
||||||
|
*/
|
||||||
|
function extractSyncSegments(data) {
|
||||||
|
const segments = [];
|
||||||
|
let remaining = data;
|
||||||
|
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
const startIdx = remaining.indexOf(DEC_SYNC_START);
|
||||||
|
|
||||||
|
if (startIdx === -1) {
|
||||||
|
// No more sync blocks, return rest as-is
|
||||||
|
if (remaining.length > 0) {
|
||||||
|
segments.push(remaining);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content before sync block (if any)
|
||||||
|
if (startIdx > 0) {
|
||||||
|
segments.push(remaining.slice(0, startIdx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching end marker
|
||||||
|
const afterStart = remaining.slice(startIdx + DEC_SYNC_START.length);
|
||||||
|
const endIdx = afterStart.indexOf(DEC_SYNC_END);
|
||||||
|
|
||||||
|
if (endIdx === -1) {
|
||||||
|
// No end marker found - sync block continues in next chunk
|
||||||
|
// Include the start marker so it can be handled when more data arrives
|
||||||
|
segments.push(remaining.slice(startIdx));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract synchronized content (without markers)
|
||||||
|
const syncContent = afterStart.slice(0, endIdx);
|
||||||
|
if (syncContent.length > 0) {
|
||||||
|
segments.push(syncContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with content after end marker
|
||||||
|
remaining = afterStart.slice(endIdx + DEC_SYNC_END.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
// Notification Manager - Multi-layer browser notification system
|
// Notification Manager - Multi-layer browser notification system
|
||||||
class NotificationManager {
|
class NotificationManager {
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
@@ -393,10 +446,11 @@ class ClaudemanApp {
|
|||||||
// Tab alert states: Map<sessionId, 'action' | 'idle'>
|
// Tab alert states: Map<sessionId, 'action' | 'idle'>
|
||||||
this.tabAlerts = new Map();
|
this.tabAlerts = new Map();
|
||||||
|
|
||||||
// Terminal write batching
|
// Terminal write batching with DEC 2026 sync support
|
||||||
this.pendingWrites = '';
|
this.pendingWrites = '';
|
||||||
this.writeFrameScheduled = false;
|
this.writeFrameScheduled = false;
|
||||||
this._wasAtBottomBeforeWrite = true; // Default to true for sticky scroll
|
this._wasAtBottomBeforeWrite = true; // Default to true for sticky scroll
|
||||||
|
this.syncWaitTimeout = null; // Timeout for incomplete sync blocks
|
||||||
|
|
||||||
// Render debouncing
|
// Render debouncing
|
||||||
this.renderSessionTabsTimeout = null;
|
this.renderSessionTabsTimeout = null;
|
||||||
@@ -620,23 +674,70 @@ class ClaudemanApp {
|
|||||||
if (!this.writeFrameScheduled) {
|
if (!this.writeFrameScheduled) {
|
||||||
this._wasAtBottomBeforeWrite = this.isTerminalAtBottom();
|
this._wasAtBottomBeforeWrite = this.isTerminalAtBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accumulate raw data (may contain DEC 2026 markers)
|
||||||
this.pendingWrites += data;
|
this.pendingWrites += data;
|
||||||
|
|
||||||
if (!this.writeFrameScheduled) {
|
if (!this.writeFrameScheduled) {
|
||||||
this.writeFrameScheduled = true;
|
this.writeFrameScheduled = true;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (this.pendingWrites && this.terminal) {
|
if (this.pendingWrites && this.terminal) {
|
||||||
this.terminal.write(this.pendingWrites);
|
// Check if we have an incomplete sync block (SYNC_START without SYNC_END)
|
||||||
this.pendingWrites = '';
|
const hasStart = this.pendingWrites.includes(DEC_SYNC_START);
|
||||||
// Sticky scroll: if user was at bottom, keep them there after new output
|
const hasEnd = this.pendingWrites.includes(DEC_SYNC_END);
|
||||||
if (this._wasAtBottomBeforeWrite) {
|
|
||||||
this.terminal.scrollToBottom();
|
if (hasStart && !hasEnd) {
|
||||||
|
// Incomplete sync block - wait for more data (up to 50ms max)
|
||||||
|
if (!this.syncWaitTimeout) {
|
||||||
|
this.syncWaitTimeout = setTimeout(() => {
|
||||||
|
this.syncWaitTimeout = null;
|
||||||
|
// Force flush after timeout to prevent stuck state
|
||||||
|
this.flushPendingWrites();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
this.writeFrameScheduled = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear any pending sync wait timeout
|
||||||
|
if (this.syncWaitTimeout) {
|
||||||
|
clearTimeout(this.syncWaitTimeout);
|
||||||
|
this.syncWaitTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flushPendingWrites();
|
||||||
}
|
}
|
||||||
this.writeFrameScheduled = false;
|
this.writeFrameScheduled = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush pending writes to terminal, processing DEC 2026 sync markers.
|
||||||
|
* Strips markers and writes content atomically within a single frame.
|
||||||
|
*/
|
||||||
|
flushPendingWrites() {
|
||||||
|
if (!this.pendingWrites || !this.terminal) return;
|
||||||
|
|
||||||
|
// Extract segments, stripping DEC 2026 markers
|
||||||
|
// This implements synchronized output for xterm.js which doesn't support DEC 2026 natively
|
||||||
|
const segments = extractSyncSegments(this.pendingWrites);
|
||||||
|
this.pendingWrites = '';
|
||||||
|
|
||||||
|
// Write all segments in a single batch (atomic within this frame)
|
||||||
|
// xterm.js internally batches multiple write() calls within same frame
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (segment && !segment.startsWith(DEC_SYNC_START)) {
|
||||||
|
this.terminal.write(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sticky scroll: if user was at bottom, keep them there after new output
|
||||||
|
if (this._wasAtBottomBeforeWrite) {
|
||||||
|
this.terminal.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write large buffer to terminal in chunks to avoid UI jank.
|
* Write large buffer to terminal in chunks to avoid UI jank.
|
||||||
* Uses requestAnimationFrame to spread work across frames.
|
* Uses requestAnimationFrame to spread work across frames.
|
||||||
@@ -651,24 +752,29 @@ class ClaudemanApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For small buffers, write directly with DEC 2026 sync for atomic render
|
// Strip any DEC 2026 markers that might be in the buffer
|
||||||
if (buffer.length <= chunkSize) {
|
// (from historical SSE data that was stored with markers)
|
||||||
this.terminal.write(DEC_SYNC_START + buffer + DEC_SYNC_END);
|
const cleanBuffer = buffer
|
||||||
|
.replaceAll(DEC_SYNC_START, '')
|
||||||
|
.replaceAll(DEC_SYNC_END, '');
|
||||||
|
|
||||||
|
// For small buffers, write directly
|
||||||
|
if (cleanBuffer.length <= chunkSize) {
|
||||||
|
this.terminal.write(cleanBuffer);
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
const writeChunk = () => {
|
const writeChunk = () => {
|
||||||
if (offset >= buffer.length) {
|
if (offset >= cleanBuffer.length) {
|
||||||
// Wait one more frame for xterm to finish rendering before resolving
|
// Wait one more frame for xterm to finish rendering before resolving
|
||||||
requestAnimationFrame(() => resolve());
|
requestAnimationFrame(() => resolve());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunk = buffer.slice(offset, offset + chunkSize);
|
const chunk = cleanBuffer.slice(offset, offset + chunkSize);
|
||||||
// Wrap each chunk with DEC 2026 sync markers for atomic render per frame
|
this.terminal.write(chunk);
|
||||||
this.terminal.write(DEC_SYNC_START + chunk + DEC_SYNC_END);
|
|
||||||
offset += chunkSize;
|
offset += chunkSize;
|
||||||
|
|
||||||
// Schedule next chunk on next frame
|
// Schedule next chunk on next frame
|
||||||
@@ -816,8 +922,12 @@ class ClaudemanApp {
|
|||||||
this.terminal.clear();
|
this.terminal.clear();
|
||||||
this.terminal.reset();
|
this.terminal.reset();
|
||||||
if (termData.terminalBuffer) {
|
if (termData.terminalBuffer) {
|
||||||
// Wrap with DEC 2026 sync markers for atomic render (prevents flicker)
|
// Strip any DEC 2026 markers and write raw content
|
||||||
this.terminal.write(DEC_SYNC_START + termData.terminalBuffer + DEC_SYNC_END);
|
// (markers don't help here - this is a static buffer reload, not live Ink redraws)
|
||||||
|
const cleanBuffer = termData.terminalBuffer
|
||||||
|
.replaceAll(DEC_SYNC_START, '')
|
||||||
|
.replaceAll(DEC_SYNC_END, '');
|
||||||
|
this.terminal.write(cleanBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send resize to ensure proper dimensions
|
// Send resize to ensure proper dimensions
|
||||||
@@ -2957,9 +3067,42 @@ class ClaudemanApp {
|
|||||||
|
|
||||||
closeRunSummary() {
|
closeRunSummary() {
|
||||||
this.runSummarySessionId = null;
|
this.runSummarySessionId = null;
|
||||||
|
this.stopRunSummaryAutoRefresh();
|
||||||
document.getElementById('runSummaryModal').classList.remove('active');
|
document.getElementById('runSummaryModal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshRunSummary() {
|
||||||
|
if (!this.runSummarySessionId) return;
|
||||||
|
await this.loadRunSummary(this.runSummarySessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRunSummaryAutoRefresh() {
|
||||||
|
const checkbox = document.getElementById('runSummaryAutoRefresh');
|
||||||
|
if (checkbox.checked) {
|
||||||
|
this.startRunSummaryAutoRefresh();
|
||||||
|
} else {
|
||||||
|
this.stopRunSummaryAutoRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startRunSummaryAutoRefresh() {
|
||||||
|
if (this.runSummaryAutoRefreshTimer) return;
|
||||||
|
this.runSummaryAutoRefreshTimer = setInterval(() => {
|
||||||
|
if (this.runSummarySessionId) {
|
||||||
|
this.loadRunSummary(this.runSummarySessionId);
|
||||||
|
}
|
||||||
|
}, 5000); // Refresh every 5 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRunSummaryAutoRefresh() {
|
||||||
|
if (this.runSummaryAutoRefreshTimer) {
|
||||||
|
clearInterval(this.runSummaryAutoRefreshTimer);
|
||||||
|
this.runSummaryAutoRefreshTimer = null;
|
||||||
|
}
|
||||||
|
const checkbox = document.getElementById('runSummaryAutoRefresh');
|
||||||
|
if (checkbox) checkbox.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
async loadRunSummary(sessionId) {
|
async loadRunSummary(sessionId) {
|
||||||
const timeline = document.getElementById('runSummaryTimeline');
|
const timeline = document.getElementById('runSummaryTimeline');
|
||||||
timeline.innerHTML = '<p class="empty-message">Loading summary...</p>';
|
timeline.innerHTML = '<p class="empty-message">Loading summary...</p>';
|
||||||
|
|||||||
@@ -514,7 +514,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<span class="run-summary-session-info" id="runSummarySessionInfo"></span>
|
<span class="run-summary-session-info" id="runSummarySessionInfo"></span>
|
||||||
<button class="btn-toolbar btn-sm" onclick="app.closeRunSummary()">Close</button>
|
<div class="modal-footer-actions">
|
||||||
|
<label class="auto-refresh-label">
|
||||||
|
<input type="checkbox" id="runSummaryAutoRefresh" onchange="app.toggleRunSummaryAutoRefresh()">
|
||||||
|
Auto-refresh
|
||||||
|
</label>
|
||||||
|
<button class="btn-toolbar btn-sm" onclick="app.refreshRunSummary()" title="Refresh summary">↻ Refresh</button>
|
||||||
|
<button class="btn-toolbar btn-sm" onclick="app.closeRunSummary()">Close</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2088,6 +2088,25 @@ body {
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-footer-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-refresh-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-refresh-label input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tab summary icon */
|
/* Tab summary icon */
|
||||||
.tab-summary {
|
.tab-summary {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user