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
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# Interactive session picker (fzf). Runs as ttyd's child when no ?arg= is given,
|
||||
# or via `devdesk pick` over SSH. Selecting an entry execs `devdesk attach NAME`.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=libexec/devdesk-lib.sh
|
||||
. "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-lib.sh"
|
||||
|
||||
DEVDESK_BIN=${DEVDESK_BIN:-devdesk}
|
||||
|
||||
entries() {
|
||||
# Output: NAME<TAB>display line (fzf shows and searches only the display line).
|
||||
"$DEVDESK_BIN" --list | jq -Mr '
|
||||
def ago: if . == null then "" else
|
||||
((now - .) | if . < 3600 then "\(. / 60 | floor)m ago"
|
||||
elif . < 86400 then "\(. / 3600 | floor)h ago"
|
||||
else "\(. / 86400 | floor)d ago" end) end;
|
||||
def pad($n): . + (" " * ([$n - length, 1] | max));
|
||||
def short: if . == null then "" else sub("^" + ($ENV.HOME // "/root"); "~") end;
|
||||
def row($mark; $color; $name; $dir; $note):
|
||||
[$name, "\u001b[38;2;\($color)m\($mark)\u001b[0m \($name | pad(26))\u001b[38;2;62;96;122m\($dir | short | pad(34))\($note)\u001b[0m"];
|
||||
(.sessions | map(.name)) as $names
|
||||
| (.sessions | sort_by(.running | not)[]
|
||||
| if .running then row("●"; "0;255;136"; .name; .dir; "\(.attached) attached · \(.activity | ago)")
|
||||
else row("○"; "62;96;122"; .name; .dir; "dormant · resumes Claude") end),
|
||||
(.projects[] | select(.name as $n | $names | index($n) | not)
|
||||
| row("+"; "0;212;255"; .name; .dir; "new session"))
|
||||
| @tsv'
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "--entries" ]; then
|
||||
entries
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Tell the web UI (via the terminal title) that the picker is showing.
|
||||
printf '\033]0;devdesk:picker|Pick a session\007'
|
||||
|
||||
while :; do
|
||||
selection=$(entries | fzf --ansi --layout=reverse --no-multi --print-query \
|
||||
--delimiter=$'\t' --with-nth=2 \
|
||||
--prompt='devdesk ▸ ' --pointer='▶' --marker='◈' \
|
||||
--header=$'Enter: open · type a new name to create it · Ctrl-R: refresh · Esc: quit\n' \
|
||||
--bind="ctrl-r:reload($0 --entries)" \
|
||||
--color='bg:#010304,bg+:#0d1520,fg:#c4d9ee,fg+:#ffffff,hl:#FF6B00,hl+:#FF8C2B' \
|
||||
--color='prompt:#FF6B00,pointer:#00D4FF,marker:#00FF88,header:#3e607a,info:#3e607a,border:#0d1520')
|
||||
rc=$?
|
||||
# Esc / Ctrl-C: leave the picker (the web UI then offers to reopen it).
|
||||
[ "$rc" -eq 130 ] && exit 0
|
||||
query=$(sed -n '1p' <<<"$selection")
|
||||
choice=$(sed -n '2p' <<<"$selection" | cut -f1)
|
||||
if [ -z "$choice" ] && [ -n "$query" ]; then
|
||||
if dd_valid_name "$query"; then
|
||||
choice=$query
|
||||
else
|
||||
printf '\n Invalid session name: %s\n' "$query"
|
||||
sleep 1.5
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
if [ -n "$choice" ]; then
|
||||
exec "$DEVDESK_BIN" attach "$choice"
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user