feat: DevDesk v0.1.0, a persistent browser terminal for Claude Code
ttyd serves a single-file xterm.js UI styled with the LotusGuild TDS (web_template base.css). Sessions live in a dedicated tmux server unit (devdesk-tmux.service), so restarting or upgrading the web unit never stops Claude. Each named session maps to a project directory. A Claude SessionStart hook records the conversation ID, so after a reboot the session resumes that exact conversation. - bin/devdesk: attach / new / ls / end / pick, with an fzf picker - ttyd requires a credential that NPM injects; origin check enabled - debian/: dh 13 with --no-stop-on-upgrade for the tmux unit, bundled sha256-verified ttyd 1.7.7, lintian overrides, man page - CI: lint, test, build, smoke-test, artifact; v* tags publish to the Gitea apt registry and create a release Refs: tinker ticket 887531976 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
# Runs in each session's "claude" window. Resumes the conversation recorded for
|
||||
# this session (see devdesk-session-hook), starts a fresh one if there is none,
|
||||
# and offers a small menu whenever Claude exits so the window never just dies.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=libexec/devdesk-lib.sh
|
||||
. "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-lib.sh"
|
||||
|
||||
NAME=${DEVDESK_SESSION:?DEVDESK_SESSION is not set}
|
||||
CLAUDE_BIN=${CLAUDE_BIN:-claude}
|
||||
SETTINGS="$DEVDESK_SHARE/claude-settings.json"
|
||||
read -r -a EXTRA <<<"${CLAUDE_ARGS:-}"
|
||||
|
||||
ORANGE=$'\033[38;2;255;107;0m'
|
||||
CYAN=$'\033[38;2;0;212;255m'
|
||||
MUTED=$'\033[38;2;62;96;122m'
|
||||
RESET=$'\033[0m'
|
||||
|
||||
common_args() {
|
||||
printf '%s\0' --settings "$SETTINGS"
|
||||
if [ "${DEVDESK_REMOTE_CONTROL:-0}" = "1" ]; then
|
||||
printf '%s\0' --remote-control "devdesk-$NAME"
|
||||
fi
|
||||
if [ ${#EXTRA[@]} -gt 0 ]; then
|
||||
printf '%s\0' "${EXTRA[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
run_new() {
|
||||
local args
|
||||
mapfile -d '' args < <(common_args)
|
||||
"$CLAUDE_BIN" -n "$NAME" "${args[@]}"
|
||||
}
|
||||
|
||||
run_resume() {
|
||||
local id args started rc
|
||||
id=$(dd_registry_get "$NAME" claude_session_id)
|
||||
if [ -z "$id" ]; then
|
||||
run_new
|
||||
return
|
||||
fi
|
||||
mapfile -d '' args < <(common_args)
|
||||
started=$SECONDS
|
||||
"$CLAUDE_BIN" --resume "$id" "${args[@]}"
|
||||
rc=$?
|
||||
# A resume that fails straight away means the transcript is gone; start fresh.
|
||||
if [ "$rc" -ne 0 ] && [ $((SECONDS - started)) -lt 10 ]; then
|
||||
printf '%sCould not resume conversation %s — starting a new one.%s\n' "$MUTED" "$id" "$RESET"
|
||||
sleep 1
|
||||
run_new
|
||||
return
|
||||
fi
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
menu() {
|
||||
printf '\n%s◈ Claude exited%s %s(session %s, code %s)%s\n' "$ORANGE" "$RESET" "$MUTED" "$NAME" "$1" "$RESET"
|
||||
printf ' %s[⏎]%s resume %s[n]%s new conversation %s[s]%s shell %s[x]%s end session\n' \
|
||||
"$CYAN" "$RESET" "$CYAN" "$RESET" "$CYAN" "$RESET" "$CYAN" "$RESET"
|
||||
}
|
||||
|
||||
printf '\033]2;%s\033\134' "$NAME" # pane title (ST = ESC \\)
|
||||
mode=resume
|
||||
while :; do
|
||||
if [ "$mode" = new ]; then run_new; else run_resume; fi
|
||||
rc=$?
|
||||
while :; do
|
||||
menu "$rc"
|
||||
IFS= read -rsn1 key || exit 0
|
||||
case $key in
|
||||
"") mode=resume; break ;;
|
||||
n | N) mode=new; break ;;
|
||||
s | S)
|
||||
printf '%sShell in %s — exit to return to this menu.%s\n' "$MUTED" "$PWD" "$RESET"
|
||||
"${SHELL:-/bin/bash}" -l
|
||||
;;
|
||||
x | X)
|
||||
printf 'End session %s? This stops Claude and forgets the session. [y/N] ' "$NAME"
|
||||
IFS= read -rsn1 confirm || exit 0
|
||||
echo
|
||||
if [ "$confirm" = y ] || [ "$confirm" = Y ]; then
|
||||
dd_registry_remove "$NAME"
|
||||
tmux kill-session -t "=$NAME" 2>/dev/null
|
||||
exit 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
done
|
||||
Reference in New Issue
Block a user