feat: v0.12.0 Settings window, per-session Remote Control, delete conversations
build-deb / Lint, test, package (push) Successful in 4m59s
build-deb / Deploy to CT 126 (push) Successful in 14s
build-deb / Notify on failure (push) Skipped

- Settings (⋯ → Settings…, palette): theme, terminal text size, interface
  size, notifications, Remote Control & privacy. The ⋯ menu shrinks.
- Remote Control per session: registry choice > Settings default >
  DEVDESK_REMOTE_CONTROL. Switching an idle session restarts its Claude
  on the same conversation; busy sessions change at the next start.
  rc_active comes from the running process; RC tag on sessions.
- --delete-convo: remove a conversation's local transcript and subagent
  files; refused while in use. Delete… in Search past conversations.
Closes #61

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHRLCCdXYm41Qx5kNhehaP
This commit is contained in:
2026-11-03 21:23:29 -05:00
co-authored by Claude Opus 5.5
parent 65114165e7
commit 20134c1e58
14 changed files with 454 additions and 50 deletions
+79 -2
View File
@@ -80,6 +80,8 @@ cmd_list() {
--argjson ctx "$(per_session_json "$DEVDESK_CONTEXT_DIR")" \
--argjson st "$(per_session_json "$DEVDESK_STATUS_DIR")" \
--argjson tags "$(per_session_json "$DEVDESK_STATE_DIR/tags")" \
--arg rclive "$(dd_rc_live)" \
--argjson rcdef "$(dd_rc_default)" \
--arg dflt "$DEVDESK_DEFAULT_SESSION" \
--arg root "$DEVDESK_PROJECTS_ROOT" '
def rows($s): $s | split("\n") | map(select(length > 0) | split("\t"));
@@ -98,7 +100,10 @@ cmd_list() {
activity: null, windows: 0,
claude_session_id: (.value.claude_session_id // null),
worktree: (.value.worktree // null)}))
) | map(. + {context: ($ctx[.name] // null), status: ($st[.name] // null), tags: ($tags[.name].hosts // [])}),
) | map(. + {context: ($ctx[.name] // null), status: ($st[.name] // null), tags: ($tags[.name].hosts // []),
remote_control: (($known[.name].remote_control) as $own | if $own == null then $rcdef else $own end),
rc_active: (.name as $n | $rclive | split("\n") | any(.[]; . == $n))}),
remote_control_default: $rcdef,
projects: ([{name: $dflt, dir: $root}]
+ [rows($proj)[] | {name: .[0], git: (.[1] == "1"), dir: (.[2:] | join("\t"))}
| select(.name | test("^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"))])}'
@@ -203,6 +208,78 @@ cmd_unsplit() {
jq -cMn --argjson c "$closed" '{ok: true, closed: $c}'
}
# Sessions whose running Claude has Remote Control on, read from the processes
# themselves (devdesk#61), one name per line.
dd_rc_live() {
local session window pid child
dd_server_up || return 0
while IFS=$'\t' read -r session window pid; do
[ "$window" = claude ] || continue
for child in $(pgrep -P "$pid" 2>/dev/null); do
if tr '\0' '\n' <"/proc/$child/cmdline" 2>/dev/null | grep -qx -- --remote-control; then
printf '%s\n' "$session"
break
fi
done
done < <(dd_tmux list-panes -a -F $'#{session_name}\t#{window_name}\t#{pane_pid}' 2>/dev/null)
}
# The Claude process of a session's claude window, if any.
dd_claude_pid() {
local pid
pid=$(dd_tmux list-panes -t "=$1:claude" -F '#{pane_pid}' 2>/dev/null | head -n1)
[ -n "$pid" ] && pgrep -P "$pid" 2>/dev/null | head -n1
}
# --remote-control NAME on|off (devdesk#61): the session's own choice. A running,
# idle Claude restarts on the same conversation right away; one that is working
# or waiting gets it at its next start. Prints {ok, remote_control, applied}.
cmd_remote_control() {
[ $# -eq 2 ] || dd_die "usage: devdesk attach --remote-control NAME on|off"
local name=$1 value applied=saved pid state
dd_valid_name "$name" || dd_die "invalid session name: $name"
case $2 in on) value=true ;; off) value=false ;; *) dd_die "on or off" ;; esac
{ session_exists "$name"; } || dd_die "no session named '$name'"
# shellcheck disable=SC2016 # jq variables
dd_registry_update '.sessions[$n] = ((.sessions[$n] // {}) + {remote_control: ($v == "true")})' --arg n "$name" --arg v "$value"
if dd_server_up && dd_tmux has-session -t "=$name" 2>/dev/null && pid=$(dd_claude_pid "$name") && [ -n "$pid" ]; then
state=$(jq -rM '.state // empty' "$DEVDESK_STATUS_DIR/$name.json" 2>/dev/null) || state="" # no state yet: idle
if [ "$state" = working ] || [ "$state" = waiting ]; then
applied=next-start
else
mkdir -p "$DEVDESK_STATE_DIR/restart"
touch "$DEVDESK_STATE_DIR/restart/$name"
kill -TERM "$pid" 2>/dev/null || true
applied=now
fi
fi
jq -cMn --argjson v "$value" --arg a "$applied" '{ok: true, remote_control: $v, applied: $a}'
}
# Remote Control default for new sessions: Settings, else DEVDESK_REMOTE_CONTROL. true/false.
dd_rc_default() {
local def
def=$(jq -rM '.remote_control_default | if . == null then empty else tostring end' "$DEVDESK_STATE_DIR/settings.json" 2>/dev/null)
if [ -n "$def" ]; then echo "$def"; elif [ "${DEVDESK_REMOTE_CONTROL:-0}" = 1 ]; then echo true; else echo false; fi
}
# --settings [remote-control-default on|off] (devdesk#61): DevDesk-wide settings
# kept in $DEVDESK_STATE_DIR/settings.json; prints them.
cmd_settings() {
local file="$DEVDESK_STATE_DIR/settings.json" tmp
if [ $# -eq 2 ]; then
[ "$1" = remote-control-default ] || dd_die "unknown setting: $1"
case $2 in on | off) ;; *) dd_die "on or off" ;; esac
tmp=$(mktemp "$DEVDESK_STATE_DIR/.settings.XXXXXX")
jq -cM --argjson v "$([ "$2" = on ] && echo true || echo false)" '. + {remote_control_default: $v}' "$file" 2>/dev/null >"$tmp" ||
jq -cMn --argjson v "$([ "$2" = on ] && echo true || echo false)" '{remote_control_default: $v}' >"$tmp"
mv -f "$tmp" "$file"
elif [ $# -ne 0 ]; then
dd_die "usage: devdesk attach --settings [remote-control-default on|off]"
fi
jq -cMn --argjson d "$(dd_rc_default)" '{ok: true, remote_control_default: $d}'
}
cmd_rename() {
[ $# -eq 2 ] || dd_die "usage: devdesk rename OLD NEW"
local old=$1 new=$2 running=0
@@ -388,7 +465,7 @@ cmd_attach() {
dd_linger
return
;;
--peek | --search | --last-reply | --unsplit)
--peek | --search | --last-reply | --unsplit | --remote-control | --settings | --delete-convo)
local action=${1#--}
shift
"cmd_${action//-/_}" "$@"