Skip to content

Instantly share code, notes, and snippets.

@safx
Last active July 22, 2025 15:13
Show Gist options
  • Save safx/6c6669735ef4b900e4fcad5f3de1be7d to your computer and use it in GitHub Desktop.
Save safx/6c6669735ef4b900e4fcad5f3de1be7d to your computer and use it in GitHub Desktop.
#!/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