qs-ask: log outcomes to journal; launcher: spinner while Claude answers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ba9558ba30
commit
748a345ea9
2 changed files with 57 additions and 13 deletions
|
|
@ -99,18 +99,32 @@
|
|||
# call fails or the model doesn't know — silence is what the launcher
|
||||
# reads as "keep the Wikipedia answer". Every reason for that silence goes
|
||||
# to STDERR instead, which the launcher discards and a human running
|
||||
# `qs-ask` by hand can read.
|
||||
# `qs-ask` by hand can read — and to the journal, since over ssh nobody
|
||||
# ever sees that STDERR. `journalctl -t qs-ask` is the only way to tell a
|
||||
# blank card caused by UNKNOWN from one caused by a failed call.
|
||||
(pkgs.writeShellScriptBin "qs-ask" ''
|
||||
# Every line carries this script's pid, because the launcher fires a
|
||||
# fresh call per debounce and concurrent runs otherwise interleave with
|
||||
# no way to pair a question with its outcome. The pid goes in the
|
||||
# message text rather than through logger's --id: --id sets SYSLOG_PID,
|
||||
# which journalctl does not display — it shows the pid of the logger
|
||||
# process itself, a different number on every line.
|
||||
log() {
|
||||
printf 'qs-ask: %s\n' "$*" >&2
|
||||
${pkgs.util-linux}/bin/logger -t qs-ask -- "[$$] $*"
|
||||
}
|
||||
|
||||
q=$(${pkgs.coreutils}/bin/cat)
|
||||
[ -z "$q" ] && { echo "qs-ask: no question on stdin" >&2; exit 0; }
|
||||
[ -z "$q" ] && { log "no question on stdin"; exit 0; }
|
||||
log "ask: $q"
|
||||
|
||||
# Claude Code derives its project state (transcripts, auto-memory) from
|
||||
# the working directory, so run from a dedicated empty one. Combined with
|
||||
# --no-session-persistence and --safe-mode, a launcher question leaves no
|
||||
# trace in the real projects' history or memory.
|
||||
work="$HOME/.cache/qs-ask"
|
||||
${pkgs.coreutils}/bin/mkdir -p "$work" || { echo "qs-ask: cannot create $work" >&2; exit 0; }
|
||||
cd "$work" || { echo "qs-ask: cannot enter $work" >&2; exit 0; }
|
||||
${pkgs.coreutils}/bin/mkdir -p "$work" || { log "cannot create $work"; exit 0; }
|
||||
cd "$work" || { log "cannot enter $work"; exit 0; }
|
||||
|
||||
# MAX_THINKING_TOKENS=0 is the single biggest win here, worth ~2.4x.
|
||||
# Measured: Haiku defaults to extended thinking, and it spent 225 output
|
||||
|
|
@ -142,14 +156,15 @@
|
|||
--name qs-ask \
|
||||
--system-prompt 'Answer the question in one or two short sentences, under 240 characters. Lead with the specific fact asked for, including units. The query is often bare keywords rather than a question ("shadow hunter knife classic wow"); treat that as a request for the key facts about that thing, not as unanswerable. No preamble, no caveats, no markdown, no follow-up offers. Reply with exactly UNKNOWN only when you genuinely do not know, or the answer depends on live data you do not have.' \
|
||||
2>/dev/null) \
|
||||
|| { echo "qs-ask: claude exited non-zero (auth expired? run 'claude' once to log in)" >&2; exit 0; }
|
||||
|| { log "claude exited non-zero (auth expired? run 'claude' once to log in)"; exit 0; }
|
||||
|
||||
ans=$(printf '%s' "$ans" | ${pkgs.coreutils}/bin/tr -d '\r' | ${pkgs.gnused}/bin/sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||
|
||||
case "$ans" in
|
||||
"") echo "qs-ask: empty answer" >&2; exit 0 ;;
|
||||
UNKNOWN*) echo "qs-ask: model replied UNKNOWN" >&2; exit 0 ;;
|
||||
"") log "empty answer"; exit 0 ;;
|
||||
UNKNOWN*) log "model replied UNKNOWN"; exit 0 ;;
|
||||
esac
|
||||
log "ok: $ans"
|
||||
printf '%s' "$ans"
|
||||
'')
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2415,7 +2415,10 @@ in
|
|||
height: answerCol.height + 28
|
||||
radius: Theme.radiusCard
|
||||
color: Theme.cardBg
|
||||
visible: launcherPanel.answerFact !== ""
|
||||
// Also up while Claude's call is out with nothing
|
||||
// on screen yet, so the spinner has somewhere to
|
||||
// live during the first second.
|
||||
visible: launcherPanel.answerFact !== "" || askProc.running
|
||||
|
||||
Column {
|
||||
id: answerCol
|
||||
|
|
@ -2451,6 +2454,7 @@ in
|
|||
// answer already came from it.
|
||||
SText {
|
||||
width: parent.width
|
||||
visible: launcherPanel.answerFact !== ""
|
||||
text: launcherPanel.answerFact
|
||||
font.pixelSize: 13
|
||||
wrapMode: Text.WordWrap
|
||||
|
|
@ -2466,13 +2470,38 @@ in
|
|||
}
|
||||
|
||||
// Always say who answered — the two sources
|
||||
// carry very different trust.
|
||||
// carry very different trust. The spinner
|
||||
// runs for as long as Claude's ~6s call is
|
||||
// out, which is the only cue that a
|
||||
// Wikipedia answer already on screen is
|
||||
// still provisional — and that a card that
|
||||
// never changes was Claude declining, not
|
||||
// Claude being slow.
|
||||
Row {
|
||||
spacing: 5
|
||||
SIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: askProc.running
|
||||
text: "loader-circle"
|
||||
font.pixelSize: 11
|
||||
color: Theme.base03
|
||||
NumberAnimation on rotation {
|
||||
from: 0; to: 360
|
||||
duration: 900
|
||||
loops: Animation.Infinite
|
||||
running: askProc.running
|
||||
}
|
||||
}
|
||||
SText {
|
||||
text: launcherPanel.answerSource
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: askProc.running
|
||||
? (launcherPanel.answerSource === "" ? "Claude" : launcherPanel.answerSource + " · Claude")
|
||||
: launcherPanel.answerSource
|
||||
font.pixelSize: 10
|
||||
color: Theme.base03
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only Wikipedia answers have somewhere to go.
|
||||
MouseArea {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue