feat: free-form sessions, rename/end from the panel, live Claude Context (0.2.0)
- Sessions no longer need a project folder: any name works and starts in DEVDESK_DEFAULT_DIR unless it matches a project; `devdesk new NAME [PROJECT]`. - Rename (✎) and end/forget (×) sessions from the Sessions panel, plus `devdesk rename`. The browser runs them as `devdesk attach --new|--rename|--end` through ttyd. Renames keep the running Claude and its recorded conversation: the hook, the run-wrapper and the statusline ask tmux for the current name. - Claude Context panel is live: devdesk-statusline snapshots model, context-window use, cost and edits per session, which --list reports; Claude's footer gets a one-line summary. The panel now sits under Sessions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+82
-35
@@ -1,18 +1,22 @@
|
||||
#!/bin/bash
|
||||
# devdesk — persistent Claude Code sessions in tmux, shared by the browser UI and SSH.
|
||||
#
|
||||
# devdesk attach [NAME] attach to session NAME (created on demand); no NAME opens the picker
|
||||
# devdesk attach --list print sessions/projects as JSON (used by the web UI over ttyd)
|
||||
# devdesk new NAME create session NAME detached, if it does not exist
|
||||
# devdesk ls list sessions
|
||||
# devdesk end NAME end session NAME and forget it
|
||||
# devdesk attach [NAME] attach to session NAME, creating it if needed; no NAME opens the picker
|
||||
# devdesk new NAME [PROJECT] create session NAME (detached) in PROJECT's folder, or the default folder
|
||||
# devdesk rename OLD NEW rename a session (running or dormant)
|
||||
# devdesk end NAME stop session NAME and forget it
|
||||
# devdesk ls list sessions
|
||||
# devdesk pick interactive picker
|
||||
#
|
||||
# The web UI calls `devdesk attach --list | --new NAME [PROJECT] | --rename OLD NEW | --end NAME`
|
||||
# through ttyd; those print JSON.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=libexec/devdesk-lib.sh
|
||||
. "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-lib.sh"
|
||||
|
||||
usage() {
|
||||
sed -n '2,9s/^# \{0,1\}//p' "$0"
|
||||
sed -n '2,10s/^# \{0,1\}//p' "$0"
|
||||
}
|
||||
|
||||
# Tab-separated running sessions: name, dir, attached clients, last activity, windows.
|
||||
@@ -36,6 +40,17 @@ projects_tsv() {
|
||||
done
|
||||
}
|
||||
|
||||
# {"<session>": {model, used_pct, ...}} from the status-line snapshots.
|
||||
context_json() {
|
||||
local f
|
||||
if [ -d "$DEVDESK_CONTEXT_DIR" ]; then
|
||||
for f in "$DEVDESK_CONTEXT_DIR"/*.json; do
|
||||
[ -f "$f" ] || continue
|
||||
jq -cM --arg n "$(basename "$f" .json)" '{($n): .}' "$f" 2>/dev/null || true
|
||||
done
|
||||
fi | jq -cMs 'add // {}'
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
local reg='{"sessions":{}}'
|
||||
[ -s "$DEVDESK_REGISTRY" ] && reg=$(cat "$DEVDESK_REGISTRY")
|
||||
@@ -43,6 +58,7 @@ cmd_list() {
|
||||
--argjson reg "$reg" \
|
||||
--arg run "$(running_tsv)" \
|
||||
--arg proj "$(projects_tsv)" \
|
||||
--argjson ctx "$(context_json)" \
|
||||
--arg dflt "$DEVDESK_DEFAULT_SESSION" \
|
||||
--arg root "$DEVDESK_PROJECTS_ROOT" '
|
||||
def rows($s): $s | split("\n") | map(select(length > 0) | split("\t"));
|
||||
@@ -59,7 +75,7 @@ cmd_list() {
|
||||
| map({name: .key, dir: .value.dir, running: false, attached: 0,
|
||||
activity: null, windows: 0,
|
||||
claude_session_id: (.value.claude_session_id // null)}))
|
||||
),
|
||||
) | map(. + {context: ($ctx[.name] // null)}),
|
||||
projects: ([{name: $dflt, dir: $root}]
|
||||
+ [rows($proj)[] | {name: .[0], dir: .[1]}
|
||||
| select(.name | test("^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"))])}'
|
||||
@@ -68,21 +84,14 @@ cmd_list() {
|
||||
cmd_ls() {
|
||||
cmd_list | jq -Mr '
|
||||
.sessions | if length == 0 then "no sessions" else
|
||||
(["", "SESSION", "CLIENTS", "DIRECTORY"] | @tsv),
|
||||
(.[] | [(if .running then "●" else "○" end), .name, (.attached | tostring), .dir] | @tsv)
|
||||
(["", "SESSION", "CLIENTS", "CONTEXT", "DIRECTORY"] | @tsv),
|
||||
(.[] | [(if .running then "●" else "○" end), .name, (.attached | tostring),
|
||||
(if .context.used_pct then "\(.context.used_pct | floor)%" else "-" end), .dir] | @tsv)
|
||||
end' | column -t -s $'\t'
|
||||
}
|
||||
|
||||
# Prints the project directory for NAME, or "" if NAME already runs and has none we can resolve.
|
||||
session_dir() {
|
||||
local name=$1 dir
|
||||
if dir=$(dd_resolve_dir "$name"); then
|
||||
printf '%s\n' "$dir"
|
||||
elif dd_tmux has-session -t "=$name" 2>/dev/null; then
|
||||
printf '%s\n' "$DEVDESK_PROJECTS_ROOT"
|
||||
else
|
||||
dd_die "no project directory for '$name' under $DEVDESK_PROJECTS_ROOT"
|
||||
fi
|
||||
session_exists() {
|
||||
{ dd_server_up && dd_tmux has-session -t "=$1" 2>/dev/null; } || [ -n "$(dd_registry_get "$1" dir)" ]
|
||||
}
|
||||
|
||||
create_detached() {
|
||||
@@ -96,27 +105,73 @@ create_detached() {
|
||||
}
|
||||
|
||||
cmd_new() {
|
||||
[ $# -eq 1 ] || dd_die "usage: devdesk new NAME"
|
||||
{ [ $# -ge 1 ] && [ $# -le 2 ]; } || dd_die "usage: devdesk new NAME [PROJECT]"
|
||||
dd_valid_name "$1" || dd_die "invalid session name: $1"
|
||||
dd_ensure_server
|
||||
local dir
|
||||
dir=$(session_dir "$1")
|
||||
if [ $# -eq 2 ]; then
|
||||
dd_valid_name "$2" || dd_die "invalid project name: $2"
|
||||
dir=$(dd_project_dir "$2") || dd_die "no project '$2' under $DEVDESK_PROJECTS_ROOT"
|
||||
! session_exists "$1" || dd_die "a session named '$1' already exists"
|
||||
else
|
||||
dir=$(dd_resolve_dir "$1")
|
||||
fi
|
||||
dd_ensure_server
|
||||
create_detached "$1" "$dir"
|
||||
}
|
||||
|
||||
cmd_rename() {
|
||||
[ $# -eq 2 ] || dd_die "usage: devdesk rename OLD NEW"
|
||||
local old=$1 new=$2 running=0
|
||||
dd_valid_name "$old" || dd_die "invalid session name: $old"
|
||||
dd_valid_name "$new" || dd_die "invalid session name: $new"
|
||||
[ "$old" != "$new" ] || return 0
|
||||
if dd_server_up && dd_tmux has-session -t "=$old" 2>/dev/null; then running=1; fi
|
||||
[ "$running" = 1 ] || [ -n "$(dd_registry_get "$old" dir)" ] || dd_die "no session named '$old'"
|
||||
! session_exists "$new" || dd_die "a session named '$new' already exists"
|
||||
if [ "$running" = 1 ]; then
|
||||
dd_tmux rename-session -t "=$old" "$new"
|
||||
dd_tmux set-environment -t "=$new" DEVDESK_SESSION "$new"
|
||||
fi
|
||||
dd_registry_rename "$old" "$new"
|
||||
}
|
||||
|
||||
cmd_end() {
|
||||
[ $# -eq 1 ] || dd_die "usage: devdesk end NAME"
|
||||
dd_valid_name "$1" || dd_die "invalid session name: $1"
|
||||
if dd_server_up; then
|
||||
dd_tmux kill-session -t "=$1" 2>/dev/null || true
|
||||
fi
|
||||
dd_registry_remove "$1"
|
||||
}
|
||||
|
||||
ok_json() {
|
||||
printf '{"ok":true}\n'
|
||||
}
|
||||
|
||||
cmd_attach() {
|
||||
if [ $# -eq 0 ]; then
|
||||
exec "$DEVDESK_LIBEXEC/devdesk-picker"
|
||||
fi
|
||||
[ $# -eq 1 ] || dd_die "attach takes exactly one session name"
|
||||
if [ "$1" = "--list" ]; then
|
||||
case $1 in
|
||||
--list)
|
||||
[ $# -eq 1 ] || dd_die "--list takes no arguments"
|
||||
cmd_list
|
||||
return
|
||||
fi
|
||||
;;
|
||||
--new | --rename | --end)
|
||||
local action=${1#--}
|
||||
shift
|
||||
"cmd_$action" "$@"
|
||||
ok_json
|
||||
return
|
||||
;;
|
||||
-*) dd_die "unknown option: $1" ;;
|
||||
esac
|
||||
[ $# -eq 1 ] || dd_die "attach takes exactly one session name"
|
||||
local name=$1 dir args
|
||||
dd_valid_name "$name" || dd_die "invalid session name: $name"
|
||||
dd_ensure_server
|
||||
dir=$(session_dir "$name")
|
||||
dir=$(dd_resolve_dir "$name")
|
||||
|
||||
# Already inside a DevDesk pane: switch this client instead of nesting.
|
||||
local outer=${TMUX:-}
|
||||
@@ -134,20 +189,12 @@ cmd_attach() {
|
||||
exec tmux -N -u -S "$DEVDESK_SOCKET" new-session -A "${args[@]}"
|
||||
}
|
||||
|
||||
cmd_end() {
|
||||
[ $# -eq 1 ] || dd_die "usage: devdesk end NAME"
|
||||
dd_valid_name "$1" || dd_die "invalid session name: $1"
|
||||
if dd_server_up; then
|
||||
dd_tmux kill-session -t "=$1" 2>/dev/null || true
|
||||
fi
|
||||
dd_registry_remove "$1"
|
||||
}
|
||||
|
||||
cmd=${1:-help}
|
||||
[ $# -gt 0 ] && shift
|
||||
case $cmd in
|
||||
attach | a) cmd_attach "$@" ;;
|
||||
new) cmd_new "$@" ;;
|
||||
rename | mv) cmd_rename "$@" ;;
|
||||
ls | list) cmd_ls ;;
|
||||
--list) cmd_list ;;
|
||||
end) cmd_end "$@" ;;
|
||||
|
||||
Reference in New Issue
Block a user