Last active
July 22, 2025 15:13
-
-
Save safx/6c6669735ef4b900e4fcad5f3de1be7d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Claude Session Selector - Interactive session selection and restore | |
set -euo pipefail | |
# Get the project directory for current path | |
PROJECT_DIR="$HOME/.claude/projects" | |
CURRENT_DIR=$(pwd) | |
PROJECT_PATH=$(echo "$CURRENT_DIR" | sed -Ee 's/[_\/]/-/g') | |
SESSION_DIR="$PROJECT_DIR/$PROJECT_PATH" | |
# Check if the session directory exists | |
if [ ! -d "$SESSION_DIR" ]; then | |
echo "Error: No Claude sessions found for current directory: $CURRENT_DIR" | |
echo "Session directory not found: $SESSION_DIR" | |
exit 1 | |
fi | |
# Function to format timestamp | |
format_timestamp() { | |
local timestamp="$1" | |
# Convert ISO timestamp to human-readable format | |
date -j -f "%Y-%m-%dT%H:%M:%S" "${timestamp%%.*}" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$timestamp" | |
} | |
# Function to extract session info from jsonl file | |
get_session_info() { | |
local file="$1" | |
local session_id | |
session_id=$(basename "$file" .jsonl) | |
# Get last user message as summary | |
local summary | |
summary=$(jq -r 'select(.type == "user") | .message.content | gsub("\n"; " ")' "$file" 2>/dev/null | tail -n 1) | |
# Get last timestamp | |
local last_timestamp | |
last_timestamp=$(jq -r 'select(.timestamp) | .timestamp' "$file" 2>/dev/null | tail -1) | |
[ -z "$summary" ] && return | |
[ -z "$last_timestamp" ] && return | |
# Format timestamp for display | |
local formatted_time | |
formatted_time=$(format_timestamp "$last_timestamp") | |
printf "%s\t%s\t%s\t%s\t%s\n" "${last_timestamp}" "${session_id}" "${formatted_time}" "${summary}" "$file" | |
} | |
# Build session list | |
get_session_list() { | |
local session_dir="$1" | |
for jsonl_file in "$session_dir"/*.jsonl; do | |
[ -f "$jsonl_file" ] || continue | |
get_session_info "$jsonl_file" | |
done | |
} | |
session_list=$(get_session_list "$SESSION_DIR" | sort -r) | |
# Use sk (skim) for interactive selection | |
selected=$(echo -e "$session_list" | sk \ | |
--no-sort \ | |
--delimiter '\t' \ | |
--nth 2,3,4 \ | |
--with-nth 3,4 \ | |
--prompt 'Session> ' \ | |
--layout 'reverse' \ | |
--preview 'lines=${FZF_PREVIEW_LINES:-${LINES:-30}}; cat {5} | jq -r " | |
select(.type == \"user\" or .type == \"assistant\") | | |
if .type == \"user\" then | |
\"π€: \" + .message.content | |
elif .type == \"assistant\" then | |
if .message.content[0].type == \"text\" then | |
\"π€: \" + .message.content[0].text | |
elif .message.content[0].type == \"thinking\" then | |
\"π Thinking...\" | |
elif .message.content[0].type == \"tool_use\" then | |
if .message.content[0].name == \"TodoWrite\" then | |
\"π TODO List:\\n\" + ( | |
.message.content[0].input.todos | | |
map(\" \" + ( | |
if .status == \"completed\" then \"β \" | |
elif .status == \"in_progress\" then \"π\" | |
else \"β³\" | |
end | |
) + \" \" + .content) | | |
join(\"\\n\") | |
) | |
elif .message.content[0].name == \"Task\" then | |
\"π§ Task: \" + .message.content[0].input.description | |
elif .message.content[0].name == \"Bash\" then | |
\"π» Cmd: \" + .message.content[0].input.command | |
elif .message.content[0].name == \"Glob\" then | |
\"π Glob: \" + .message.content[0].input.pattern + (if .message.content[0].input.path then \" in \" + .message.content[0].input.path else \"\" end) | |
elif .message.content[0].name == \"Grep\" then | |
\"π Grep: \" + .message.content[0].input.pattern + (if .message.content[0].input.path then \" in \" + .message.content[0].input.path else \"\" end) | |
elif .message.content[0].name == \"LS\" then | |
\"π LS: \" + .message.content[0].input.path | |
elif .message.content[0].name == \"ExitPlanMode\" then | |
\"β Exit Plan Mode\" | |
elif .message.content[0].name == \"Read\" then | |
\"π Read: \" + .message.content[0].input.file_path | |
elif .message.content[0].name == \"Edit\" then | |
\"βοΈ Edit: \" + .message.content[0].input.file_path | |
elif .message.content[0].name == \"MultiEdit\" then | |
\"π MultiEdit: \" + .message.content[0].input.file_path + \" (\" + (.message.content[0].input.edits | length | tostring) + \" edits)\" | |
elif .message.content[0].name == \"Write\" then | |
\"πΎ Write: \" + .message.content[0].input.file_path | |
elif .message.content[0].name == \"WebFetch\" then | |
\"π WebFetch: \" + .message.content[0].input.url | |
elif .message.content[0].name == \"WebSearch\" then | |
\"π Search: \" + .message.content[0].input.query | |
else | |
\"[\" + .message.content[0].name + \"] \" + (.message.content[0].input | tostring) | |
end | |
else | |
.message.content | tostring | |
end | |
else | |
empty | |
end" 2>/dev/null | sed "s/<command-name>\([^<]*\)<\/command-name>/π Command: \1/g; s/<command-args>\([^<]*\)<\/command-args>/\nπ Args: \1/g" | perl -pe '"'"'s/\*\*([^*]+)\*\*/\033[1m$1\033[0m/g'"'"' | tail -n $((lines - 2))' \ | |
--preview-window 'right:60%:wrap' \ | |
--ansi) | |
# Extract session ID from selection | |
if [ -n "$selected" ]; then | |
session_id=$(echo "$selected" | awk -F'\t' '{print $2}') | |
if [ "${1:-}" == "-s" ] ; then | |
echo "$session_id" | |
exit 0 | |
fi | |
claude -r "$session_id" | |
else | |
exit 0 | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment