@@ -1912,6 +1912,7 @@ get_wip_dir() {
19121912wip_save () {
19131913 local num=$1
19141914 local do_restart=$2
1915+ local custom_name=" ${3:- } "
19151916 local dir=" $WORKSPACE_BASE /$WORKSPACE_PREFIX -$num "
19161917 local wip_dir=$( get_wip_dir " $num " )
19171918 local timestamp=$( date +%Y%m%d-%H%M%S)
@@ -1930,6 +1931,9 @@ wip_save() {
19301931 local status=$( git status --porcelain 2> /dev/null || echo " " )
19311932 local branch=$( git branch --show-current 2> /dev/null || echo " unknown" )
19321933
1934+ # Get untracked files
1935+ local untracked_files=$( git ls-files --others --exclude-standard 2> /dev/null || echo " " )
1936+
19331937 git fetch origin main --quiet 2> /dev/null || true
19341938 local commits_ahead=$( git rev-list --count origin/main..HEAD 2> /dev/null || echo " 0" )
19351939 local commit_log=" "
@@ -1956,7 +1960,7 @@ wip_save() {
19561960
19571961 # Check if there's anything to save
19581962 local has_changes=" false"
1959- if [ -n " $diff " ] || [ -n " $staged " ] || [ -n " $status " ] || [ -n " $submodule_diffs " ]; then
1963+ if [ -n " $diff " ] || [ -n " $staged " ] || [ -n " $status " ] || [ -n " $submodule_diffs " ] || [ -n " $untracked_files " ] ; then
19601964 has_changes=" true"
19611965 elif [ " $commits_ahead " -gt 0 ] 2> /dev/null; then
19621966 has_changes=" true"
@@ -1969,28 +1973,57 @@ wip_save() {
19691973 return 0
19701974 fi
19711975
1972- local slug=" wip- $( date +%H%M ) "
1973- local summary=" Work in progress "
1976+ local slug=" "
1977+ local summary=" "
19741978
1975- # Try to generate AI summary if claude is available
1976- if command_exists claude; then
1977- local all_diffs=" $diff$staged$submodule_diffs "
1978- local summary_json=$( generate_wip_summary " $all_diffs " )
1979- local parsed_slug=$( echo " $summary_json " | sed -n ' s/.*"slug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' )
1980- local parsed_summary=$( echo " $summary_json " | sed -n ' s/.*"summary"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' )
1981- [ -n " $parsed_slug " ] && slug=" $parsed_slug "
1982- [ -n " $parsed_summary " ] && summary=" $parsed_summary "
1979+ # Use custom name if provided
1980+ if [ -n " $custom_name " ]; then
1981+ slug=$( echo " $custom_name " | tr ' ' ' -' | tr ' [:upper:]' ' [:lower:]' | sed ' s/[^a-z0-9-]//g' )
1982+ summary=" $custom_name "
1983+ else
1984+ # Try to generate AI summary if claude is available
1985+ slug=" wip-$( date +%H%M) "
1986+ summary=" Work in progress"
1987+ if command_exists claude; then
1988+ local all_diffs=" $diff$staged$submodule_diffs "
1989+ local summary_json=$( generate_wip_summary " $all_diffs " )
1990+ local parsed_slug=$( echo " $summary_json " | sed -n ' s/.*"slug"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' )
1991+ local parsed_summary=$( echo " $summary_json " | sed -n ' s/.*"summary"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' )
1992+ [ -n " $parsed_slug " ] && slug=" $parsed_slug "
1993+ [ -n " $parsed_summary " ] && summary=" $parsed_summary "
1994+ fi
19831995 fi
19841996
19851997 local wip_path=" $wip_dir /$timestamp -$slug "
19861998 mkdir -p " $wip_path "
19871999
1988- echo " Saving patches..."
1989- [ -n " $diff " ] && echo " $diff " > " $wip_path /main-unstaged.patch"
1990- [ -n " $staged " ] && echo " $staged " > " $wip_path /main-staged.patch"
2000+ local saved_count=0
2001+
2002+ # Save patches
2003+ if [ -n " $diff " ]; then
2004+ echo " $diff " > " $wip_path /main-unstaged.patch"
2005+ (( saved_count++ ))
2006+ fi
2007+ if [ -n " $staged " ]; then
2008+ echo " $staged " > " $wip_path /main-staged.patch"
2009+ (( saved_count++ ))
2010+ fi
19912011 [ -n " $status " ] && echo " $status " > " $wip_path /main-status.txt"
19922012 [ -n " $commit_log " ] && echo " $commit_log " > " $wip_path /main-commits.txt"
19932013
2014+ # Save untracked files (copy actual content)
2015+ if [ -n " $untracked_files " ]; then
2016+ local untracked_dir=" $wip_path /untracked"
2017+ mkdir -p " $untracked_dir "
2018+ echo " $untracked_files " | while read -r file; do
2019+ [ -z " $file " ] && continue
2020+ local file_dir=$( dirname " $file " )
2021+ mkdir -p " $untracked_dir /$file_dir "
2022+ cp " $dir /$file " " $untracked_dir /$file " 2> /dev/null && (( saved_count++ ))
2023+ done
2024+ echo " $untracked_files " > " $wip_path /untracked-files.txt"
2025+ fi
2026+
19942027 # Save submodule patches
19952028 for (( i= 0 ; i< submodules_count; i++ )) ; do
19962029 local sub_path=$( yq -r " .submodules[$i ].path" " $CONFIG_FILE " 2> /dev/null)
@@ -2025,8 +2058,7 @@ EOF
20252058 mood_record_event " wip_save" " $slug "
20262059
20272060 success " WIP saved: $slug "
2028- echo -e " ${BLUE} $summary ${NC} "
2029- echo " Location: $wip_path "
2061+ echo -e " ${GRAY} $summary ${NC} "
20302062
20312063 if [ " $do_restart " = " true" ]; then
20322064 echo " "
@@ -2600,16 +2632,26 @@ _restore_wip() {
26002632 fi
26012633 done
26022634
2635+ # Restore untracked files
2636+ if [ -d " $wip_path /untracked" ]; then
2637+ echo " Restoring untracked files..."
2638+ cp -r " $wip_path /untracked/." " $dir /" 2> /dev/null || true
2639+ fi
2640+
26032641 success " WIP restored: $wip_name "
26042642
2605- if [ " $open_after " = " true" ]; then
2643+ # Check if user is already in the workspace
2644+ local current_dir=$( pwd)
2645+ if [[ " $current_dir " == " $dir " * ]]; then
2646+ # Already in workspace, no need to tell them to open it
2647+ :
2648+ elif [ " $open_after " = " true" ]; then
26062649 echo " "
26072650 echo " Opening workspace $num ..."
26082651 open_workspace " $num "
26092652 else
26102653 echo " "
26112654 echo " Run 'crab $num ' to open the workspace"
2612- echo " Or 'crab wip restore <N> --open' to restore and open"
26132655 fi
26142656}
26152657
@@ -6400,11 +6442,22 @@ handle_wip_for_workspace() {
64006442
64016443 case " ${1:- } " in
64026444 " save" )
6403- if [ " ${2:- } " = " --restart" ] || [ " ${2:- } " = " -r" ]; then
6404- wip_save " $num " " true"
6405- else
6406- wip_save " $num " " false"
6407- fi
6445+ shift # Remove "save"
6446+ local do_restart=" false"
6447+ local custom_name=" "
6448+ while [ $# -gt 0 ]; do
6449+ case " $1 " in
6450+ --restart|-r)
6451+ do_restart=" true"
6452+ shift
6453+ ;;
6454+ * )
6455+ custom_name=" $1 "
6456+ shift
6457+ ;;
6458+ esac
6459+ done
6460+ wip_save " $num " " $do_restart " " $custom_name "
64086461 ;;
64096462 " ls" |" list" )
64106463 wip_list " $num "
@@ -6420,7 +6473,7 @@ handle_wip_for_workspace() {
64206473 ;;
64216474 * )
64226475 echo -e " ${CYAN} WIP Commands for workspace $num :${NC} "
6423- echo " crab ws $num wip save [--restart]"
6476+ echo " crab ws $num wip save [\" name \" ] [ --restart]"
64246477 echo " crab ws $num wip ls"
64256478 echo " crab ws $num wip --continue"
64266479 echo " crab ws $num wip --resume"
@@ -7373,7 +7426,7 @@ main() {
73737426
73747427 # For project-aware commands, resolve project (cwd-first, then default)
73757428 case " ${1:- } " in
7376- " " |" ws" |" workspace" |" restart" |" reset" |" refresh" |" continue" |" resume" |" cleanup" |" clean" |" destroy" |" rm" |" remove" |" new" |" create" |" wip" |" config" |" doctor" |" ports" |" shared" |" status" |" snapshot" |" receive" |" handoff" |" rewind" |" timetravel" |" tt" |" pair" |" join" |" spectate" |" watch" |" mood" |" mobile" |" slack" |" tk" |" toolkit" |" pf" |" promptfoo" |" session" |" review" )
7429+ " " |" ws" |" workspace" |" restart" |" reset" |" refresh" |" continue" |" resume" |" cleanup" |" clean" |" destroy" |" rm" |" remove" |" new" |" create" |" wip" |" save " | " config" |" doctor" |" ports" |" shared" |" status" |" snapshot" |" receive" |" handoff" |" rewind" |" timetravel" |" tt" |" pair" |" join" |" spectate" |" watch" |" mood" |" mobile" |" slack" |" tk" |" toolkit" |" pf" |" promptfoo" |" session" |" review" )
73777430 if [ -z " $PROJECT_ALIAS " ]; then
73787431 # Legacy migration check
73797432 if is_legacy_config; then
@@ -7481,6 +7534,32 @@ main() {
74817534 fi
74827535 cleanup_workspace " $num "
74837536 ;;
7537+ " save" )
7538+ # Shortcut: crab save ["name"] [--restart]
7539+ load_config
7540+ validate_config
7541+ num=$( detect_workspace)
7542+ if [ -z " $num " ]; then
7543+ error " Not in a workspace directory. Run from inside a workspace."
7544+ exit 1
7545+ fi
7546+ local do_restart=" false"
7547+ local custom_name=" "
7548+ shift # Remove "save"
7549+ while [ $# -gt 0 ]; do
7550+ case " $1 " in
7551+ --restart|-r)
7552+ do_restart=" true"
7553+ shift
7554+ ;;
7555+ * )
7556+ custom_name=" $1 "
7557+ shift
7558+ ;;
7559+ esac
7560+ done
7561+ wip_save " $num " " $do_restart " " $custom_name "
7562+ ;;
74847563 " destroy" |" rm" |" remove" )
74857564 load_config
74867565 validate_config
@@ -7507,11 +7586,24 @@ main() {
75077586 error " Not in a workspace directory. Use: crab ws <N> wip save"
75087587 exit 1
75097588 fi
7510- if [ " ${3:- } " = " --restart" ] || [ " ${3:- } " = " -r" ]; then
7511- wip_save " $num " " true"
7512- else
7513- wip_save " $num " " false"
7514- fi
7589+ # Parse: crab wip save ["name"] [--restart]
7590+ local do_restart=" false"
7591+ local custom_name=" "
7592+ shift 2 # Remove "wip" and "save"
7593+ while [ $# -gt 0 ]; do
7594+ case " $1 " in
7595+ --restart|-r)
7596+ do_restart=" true"
7597+ shift
7598+ ;;
7599+ * )
7600+ # Anything else is the custom name
7601+ custom_name=" $1 "
7602+ shift
7603+ ;;
7604+ esac
7605+ done
7606+ wip_save " $num " " $do_restart " " $custom_name "
75157607 ;;
75167608 " ls" |" list" )
75177609 # List is global by default, unless --ws flag or physically in workspace
0 commit comments