aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/ai/ai-state.sh
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-05 12:50:37 +0200
committerDanilo M. <danix@danix.xyz>2026-10-05 12:50:37 +0200
commitc4e3406c8241ada9b9d7069149f88ab55c3b5952 (patch)
treeed89a14c83f6c9a858523b2abb66d37fe123ed46 /desktop/modules/ai/ai-state.sh
parent69f34da5a290b0f12b65242ae6ad21491d182a71 (diff)
downloadquickshell-c4e3406c8241ada9b9d7069149f88ab55c3b5952.tar.gz
quickshell-c4e3406c8241ada9b9d7069149f88ab55c3b5952.zip
feat(ai): state sweep for the AI stack
Six TSV lines per sweep: llama and sd engines, four apps. llama's slot probe passes autoload=false, since a plain /slots?model= loads the model into VRAM. sd-server has no progress endpoint, so busy is its log having been written in the last 5s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/ai/ai-state.sh')
-rwxr-xr-xdesktop/modules/ai/ai-state.sh106
1 files changed, 106 insertions, 0 deletions
diff --git a/desktop/modules/ai/ai-state.sh b/desktop/modules/ai/ai-state.sh
new file mode 100755
index 0000000..b3b9642
--- /dev/null
+++ b/desktop/modules/ai/ai-state.sh
@@ -0,0 +1,106 @@
+#!/bin/bash
+#
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# One sweep of the local AI stack for the drawer's AI page. Always prints
+# these six lines, tab separated, and exits 0:
+#
+# engine<TAB>llama<TAB>up|down<TAB>idle|busy<TAB>model
+# engine<TAB>sd<TAB>up|down<TAB>idle|busy<TAB>model
+# app<TAB>assistant<TAB>running|stopped<TAB>-
+# app<TAB>chat<TAB>running|stopped<TAB>-
+# app<TAB>imggen<TAB>running|stopped<TAB>model
+# app<TAB>fanfic<TAB>running|stopped<TAB>pid
+#
+# An empty value is "-". A probe that fails reads as down or stopped, not as
+# an error: every row is something that may simply not be running.
+#
+# The test stubs curl, pgrep, llamachat and imggen on PATH and points
+# AI_ASSISTANT and AI_SD_LOG at fixtures; see test-ai-state.sh.
+
+set -u
+
+LLAMA="${AI_LLAMA_URL:-http://127.0.0.1:8181}"
+SD_LOG="${AI_SD_LOG:-${XDG_CACHE_HOME:-$HOME/.cache}/sd-server.log}"
+ASSISTANT="${AI_ASSISTANT:-$HOME/Programming/GIT/desktop-assistant/assistant.sh}"
+
+row() { local IFS=$'\t'; printf '%s\n' "$*"; }
+
+llama() {
+ local models id busy=idle
+ models="$(curl -sf -m 2 "$LLAMA/models")" || { row engine llama down idle -; return; }
+ id="$(jq -r 'first(.data[] | select(.status.value == "loaded") | .id) // empty' \
+ <<<"$models" 2>/dev/null)"
+ # autoload=false is not optional: without it this probe loads an unloaded
+ # model into VRAM, and the model can unload between the two requests.
+ if [[ -n "$id" ]] &&
+ curl -sf -m 2 -G --data-urlencode "model=$id" -d autoload=false "$LLAMA/slots" |
+ jq -e 'any(.[]; .is_processing)' >/dev/null 2>&1; then
+ busy=busy
+ fi
+ row engine llama up "$busy" "${id:--}"
+}
+
+sd() {
+ local line words i model=- busy=idle mtime
+ line="$(pgrep -axo sd-server)" || { row engine sd down idle -; return; }
+ read -ra words <<<"$line"
+ for ((i = 1; i < ${#words[@]} - 1; i++)); do
+ case "${words[i]}" in
+ --diffusion-model|-m) model="${words[i + 1]##*/}" ;;
+ esac
+ done
+ # ponytail: sd-server has no progress endpoint, but its log streams
+ # progress bars while generating, so a log written in the last 5s means
+ # busy. A step slower than 5s reads as idle.
+ mtime="$(stat -c %Y "$SD_LOG" 2>/dev/null)" || mtime=0
+ (( $(date +%s) - mtime < 5 )) && busy=busy
+ row engine sd up "$busy" "$model"
+}
+
+apps() {
+ local out pid
+
+ if "$ASSISTANT" status >/dev/null 2>&1; then
+ row app assistant running -
+ else
+ row app assistant stopped -
+ fi
+
+ if llamachat --ping >/dev/null 2>&1; then
+ row app chat running -
+ else
+ row app chat stopped -
+ fi
+
+ # imggen status exits 0 either way: the daemon's JSON when it is up,
+ # "daemon down" when it is not.
+ out="$(imggen status 2>/dev/null)"
+ if [[ "$out" == "{"* ]]; then
+ row app imggen running "$(jq -r '.model // "-"' <<<"$out" 2>/dev/null || echo -)"
+ else
+ row app imggen stopped -
+ fi
+
+ # It runs through env, so the command line is python3 then the script,
+ # by path from ~/bin or by ./fanfictioner from the repo.
+ if pid="$(pgrep -u "$USER" -fo '^[^ ]*python3[^ ]* [^ ]*fanfictioner( |$)')"; then
+ row app fanfic running "$pid"
+ else
+ row app fanfic stopped -
+ fi
+}
+
+llama
+sd
+apps
+exit 0