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:
+83
-9
@@ -6,10 +6,12 @@
|
||||
# devdesk rename OLD NEW rename a session (running or dormant)
|
||||
# devdesk end NAME stop session NAME and forget it
|
||||
# devdesk repo NAME git + Gitea CI status of the session's folder (JSON)
|
||||
# devdesk alerts firing/pending Grafana alerts (JSON)
|
||||
# devdesk ls list sessions
|
||||
# devdesk pick interactive picker
|
||||
#
|
||||
# The web UI calls `devdesk attach --list | --repo NAME | --new NAME [PROJECT] | --rename OLD NEW | --end NAME`
|
||||
# The web UI calls `devdesk attach --list | --repo NAME | --alerts | --upload SESSION FILE BYTES |
|
||||
# --new NAME [PROJECT] | --rename OLD NEW | --end NAME`
|
||||
# through ttyd; those print JSON.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -17,7 +19,7 @@ set -euo pipefail
|
||||
. "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-lib.sh"
|
||||
|
||||
usage() {
|
||||
sed -n '2,11s/^# \{0,1\}//p' "$0"
|
||||
sed -n '2,12s/^# \{0,1\}//p' "$0"
|
||||
}
|
||||
|
||||
# Tab-separated running sessions: name, dir, attached clients, last activity, windows.
|
||||
@@ -41,11 +43,12 @@ 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
|
||||
# {"<session>": {...}} merged from the per-session JSON files in DIR
|
||||
# (status-line snapshots, hook states).
|
||||
per_session_json() {
|
||||
local dir=$1 f
|
||||
if [ -d "$dir" ]; then
|
||||
for f in "$dir"/*.json; do
|
||||
[ -f "$f" ] || continue
|
||||
jq -cM --arg n "$(basename "$f" .json)" '{($n): .}' "$f" 2>/dev/null || true
|
||||
done
|
||||
@@ -59,7 +62,8 @@ cmd_list() {
|
||||
--argjson reg "$reg" \
|
||||
--arg run "$(running_tsv)" \
|
||||
--arg proj "$(projects_tsv)" \
|
||||
--argjson ctx "$(context_json)" \
|
||||
--argjson ctx "$(per_session_json "$DEVDESK_CONTEXT_DIR")" \
|
||||
--argjson st "$(per_session_json "$DEVDESK_STATUS_DIR")" \
|
||||
--arg dflt "$DEVDESK_DEFAULT_SESSION" \
|
||||
--arg root "$DEVDESK_PROJECTS_ROOT" '
|
||||
def rows($s): $s | split("\n") | map(select(length > 0) | split("\t"));
|
||||
@@ -76,7 +80,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)}),
|
||||
) | map(. + {context: ($ctx[.name] // null), status: ($st[.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}$"))])}'
|
||||
@@ -206,6 +210,66 @@ cmd_repo() {
|
||||
prs: $prs}'
|
||||
}
|
||||
|
||||
# Firing and pending Grafana alert rules, most severe first.
|
||||
cmd_alerts() {
|
||||
if [ -z "$DEVDESK_GRAFANA_URL" ]; then
|
||||
jq -cMn '{configured: false, alerts: []}'
|
||||
return
|
||||
fi
|
||||
local auth=() raw
|
||||
if [ -r "$DEVDESK_GRAFANA_TOKEN_FILE" ]; then
|
||||
auth=(-H "Authorization: Bearer $(tr -d '[:space:]' <"$DEVDESK_GRAFANA_TOKEN_FILE")")
|
||||
fi
|
||||
if ! raw=$(curl -fsS --max-time 6 "${auth[@]}" "$DEVDESK_GRAFANA_URL/api/prometheus/grafana/api/v1/rules" 2>&1); then
|
||||
jq -cMn --arg e "${raw:-no response}" '{configured: true, error: $e, alerts: []}'
|
||||
return
|
||||
fi
|
||||
jq -cM --arg pub "$DEVDESK_GRAFANA_PUBLIC_URL" '
|
||||
def num: (tonumber? // .) | tostring;
|
||||
{configured: true, checked: (now | floor), alerts: [
|
||||
.data.groups[] | .name as $group | .file as $folder | .rules[]
|
||||
| select(.state == "firing" or .state == "pending")
|
||||
| . as $r
|
||||
| ([.alerts[]? | select(.state == "Alerting" or .state == "Pending")]) as $active
|
||||
| {name, state, severity: (.labels.severity // null), group: $group, folder: $folder,
|
||||
since: (.activeAt // $active[0].activeAt // null),
|
||||
summary: ((.annotations.summary // .annotations.description // "")
|
||||
| gsub("\\{\\{ *\\$value *\\}\\}"; ($active[0].value // "?" | num))),
|
||||
instances: [$active[] | .labels
|
||||
| del(.__alert_rule_uid__, .__grafana_origin__, .alertname, .grafana_folder, .__name__, .severity)
|
||||
| to_entries | map("\(.key)=\(.value)") | join(" ")
|
||||
| select(. != "")],
|
||||
url: "\($pub)/alerting/grafana/\(.uid)/view"}
|
||||
] | sort_by(if .severity == "critical" then 0 elif .severity == "warning" then 1 else 2 end)}' <<<"$raw"
|
||||
}
|
||||
|
||||
# Receive a file for SESSION over the terminal: the browser waits for the ready
|
||||
# line, then sends exactly ceil(BYTES/3)*4 bytes of base64 as input.
|
||||
cmd_upload() {
|
||||
[ $# -eq 3 ] || dd_die "usage: devdesk attach --upload SESSION FILENAME BYTES"
|
||||
local session=$1 name size=$3 dir dest b64len actual
|
||||
dd_valid_name "$session" || dd_die "invalid session name: $session"
|
||||
{ [[ $size =~ ^[0-9]+$ ]] && [ "$size" -gt 0 ] && [ "$size" -le "$DEVDESK_UPLOAD_MAX" ]; } ||
|
||||
dd_die "file is empty or larger than $((DEVDESK_UPLOAD_MAX / 1048576)) MB"
|
||||
name=$(dd_safe_filename "$2")
|
||||
dir="$DEVDESK_STATE_DIR/uploads/$session"
|
||||
mkdir -p "$dir"
|
||||
find "$DEVDESK_STATE_DIR/uploads" -type f -mtime +7 -delete 2>/dev/null || true
|
||||
dest="$dir/$(date +%Y%m%d-%H%M%S)-$name"
|
||||
# shellcheck disable=SC2017 # integer ceil(size/3)*4: the base64 length, on purpose
|
||||
b64len=$(((size + 2) / 3 * 4))
|
||||
stty raw -echo 2>/dev/null || true
|
||||
printf 'DEVDESK-UPLOAD-READY\r\n'
|
||||
head -c "$b64len" | base64 -d >"$dest" 2>/dev/null || true
|
||||
stty sane 2>/dev/null || true
|
||||
actual=$(stat -c %s "$dest" 2>/dev/null || echo 0)
|
||||
if [ "$actual" != "$size" ]; then
|
||||
rm -f "$dest"
|
||||
dd_die "upload incomplete ($actual of $size bytes)"
|
||||
fi
|
||||
jq -cMn --arg p "$dest" '{ok: true, path: $p}'
|
||||
}
|
||||
|
||||
ok_json() {
|
||||
printf '{"ok":true}\n'
|
||||
}
|
||||
@@ -225,6 +289,15 @@ cmd_attach() {
|
||||
cmd_repo "$@"
|
||||
return
|
||||
;;
|
||||
--alerts)
|
||||
cmd_alerts
|
||||
return
|
||||
;;
|
||||
--upload)
|
||||
shift
|
||||
cmd_upload "$@"
|
||||
return
|
||||
;;
|
||||
--new | --rename | --end)
|
||||
local action=${1#--}
|
||||
shift
|
||||
@@ -263,6 +336,7 @@ attach | a) cmd_attach "$@" ;;
|
||||
new) cmd_new "$@" ;;
|
||||
rename | mv) cmd_rename "$@" ;;
|
||||
repo) cmd_repo "$@" ;;
|
||||
alerts) cmd_alerts ;;
|
||||
ls | list) cmd_ls ;;
|
||||
--list) cmd_list ;;
|
||||
end) cmd_end "$@" ;;
|
||||
|
||||
Reference in New Issue
Block a user