feat: session status at a glance, file & image drop, live Ops Alerts (0.4.0)
- devdesk-state-hook (Claude UserPromptSubmit/PostToolUse/Notification/Stop/ SessionStart/SessionEnd) records whether each session is working, waiting on you or idle; --list carries it. The sidebar shows it, the header shows the current session's state, and a pulsing "needs you" chip jumps to sessions waiting for input. Session refresh no longer depends on the sidebar being open. - `devdesk attach --upload SESSION FILE BYTES` receives a file as base64 terminal input (raw mode, exact length, size-checked, sanitised name) into /var/lib/devdesk/uploads/<session>/. The UI uploads dropped or pasted files and pastes the path into Claude, which attaches images itself. - `devdesk alerts` / `--alerts`: firing and pending Grafana alert rules with severity, instances, summary and a link. The panel's "Ask Claude" types an investigation prompt into the current session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+21
-4
@@ -30,9 +30,14 @@ dd_load_env
|
||||
: "${DEVDESK_GITEA_URL:=https://code.lotusguild.org}"
|
||||
: "${DEVDESK_GITEA_ALIASES:=10.10.10.63}"
|
||||
: "${DEVDESK_GITEA_TOKEN_FILE:=/etc/devdesk/gitea-token}"
|
||||
: "${DEVDESK_GRAFANA_URL:=}"
|
||||
: "${DEVDESK_GRAFANA_PUBLIC_URL:=$DEVDESK_GRAFANA_URL}"
|
||||
: "${DEVDESK_GRAFANA_TOKEN_FILE:=/etc/devdesk/grafana-token}"
|
||||
: "${DEVDESK_UPLOAD_MAX:=26214400}"
|
||||
|
||||
DEVDESK_REGISTRY="$DEVDESK_STATE_DIR/registry.json"
|
||||
DEVDESK_CONTEXT_DIR="$DEVDESK_STATE_DIR/context"
|
||||
DEVDESK_STATUS_DIR="$DEVDESK_STATE_DIR/state"
|
||||
DEVDESK_NAME_RE='^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$'
|
||||
|
||||
dd_die() {
|
||||
@@ -113,15 +118,18 @@ dd_registry_set_claude() {
|
||||
|
||||
dd_registry_remove() {
|
||||
dd_registry_update 'del(.sessions[$n])' --arg n "$1"
|
||||
rm -f "$DEVDESK_CONTEXT_DIR/$1.json"
|
||||
rm -f "$DEVDESK_CONTEXT_DIR/$1.json" "$DEVDESK_STATUS_DIR/$1.json"
|
||||
}
|
||||
|
||||
dd_registry_rename() {
|
||||
dd_registry_update 'if .sessions[$old] then .sessions[$new] = .sessions[$old] | del(.sessions[$old]) else . end' \
|
||||
--arg old "$1" --arg new "$2"
|
||||
if [ -f "$DEVDESK_CONTEXT_DIR/$1.json" ]; then
|
||||
mv -f "$DEVDESK_CONTEXT_DIR/$1.json" "$DEVDESK_CONTEXT_DIR/$2.json"
|
||||
fi
|
||||
local d
|
||||
for d in "$DEVDESK_CONTEXT_DIR" "$DEVDESK_STATUS_DIR"; do
|
||||
if [ -f "$d/$1.json" ]; then
|
||||
mv -f "$d/$1.json" "$d/$2.json"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Project name (directory or its slug) -> absolute directory inside the projects root.
|
||||
@@ -147,6 +155,15 @@ dd_project_dir() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Upload filename -> safe basename ([A-Za-z0-9._-], no leading dots, max 100 chars).
|
||||
dd_safe_filename() {
|
||||
local n=${1##*/}
|
||||
n=${n//[^A-Za-z0-9._-]/_}
|
||||
while [[ $n == .* ]]; do n=${n:1}; done
|
||||
n=${n:0:100}
|
||||
printf '%s' "${n:-upload}"
|
||||
}
|
||||
|
||||
# Session name -> working directory. Sessions don't need a project: the
|
||||
# registry wins, then a project of the same name, then DEVDESK_DEFAULT_DIR.
|
||||
dd_resolve_dir() {
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Claude Code hook (wired in via claude-settings.json) that tracks what Claude is
|
||||
# doing in each DevDesk session, for the sidebar's at-a-glance status:
|
||||
# UserPromptSubmit, PostToolUse -> working
|
||||
# Notification -> waiting (Claude needs input or a permission)
|
||||
# Stop, SessionStart -> idle
|
||||
# SessionEnd -> exited
|
||||
# Fires on every tool call, so it stays tiny and only writes when the state changes.
|
||||
|
||||
input=$(cat)
|
||||
|
||||
outer=${TMUX:-}
|
||||
[ "${outer%%,*}" = "${DEVDESK_SOCKET:-/run/devdesk/tmux.sock}" ] && [ -n "${TMUX_PANE:-}" ] || exit 0
|
||||
session=$(tmux display-message -p -t "$TMUX_PANE" '#S' 2>/dev/null)
|
||||
[[ $session =~ ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$ ]] || exit 0
|
||||
|
||||
IFS=$'\t' read -r event message < <(jq -rM '[.hook_event_name // "", (.message // "" | gsub("[\t\n]"; " "))] | @tsv' <<<"$input" 2>/dev/null)
|
||||
case $event in
|
||||
UserPromptSubmit | PostToolUse) state=working ;;
|
||||
Notification) state=waiting ;;
|
||||
Stop | SessionStart) state=idle ;;
|
||||
SessionEnd) state=exited ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
dir="${DEVDESK_STATE_DIR:-/var/lib/devdesk}/state"
|
||||
file="$dir/$session.json"
|
||||
# Unchanged state (the common case during tool runs): nothing to write.
|
||||
if [ "$state" != waiting ] && [ -f "$file" ] && grep -q "\"state\":\"$state\"" "$file"; then
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p "$dir" 2>/dev/null || exit 0
|
||||
tmp=$(mktemp "$dir/.$session.XXXXXX" 2>/dev/null) || exit 0
|
||||
if jq -cMn --arg s "$state" --arg m "$message" --arg e "$event" \
|
||||
'{state: $s, since: (now | floor), event: $e, message: (if $m == "" then null else $m end)}' >"$tmp"; then
|
||||
mv -f "$tmp" "$file"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user