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
|
# 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
|
# reads as "keep the Wikipedia answer". Every reason for that silence goes
|
||||||
# to STDERR instead, which the launcher discards and a human running
|
# 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" ''
|
(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)
|
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
|
# Claude Code derives its project state (transcripts, auto-memory) from
|
||||||
# the working directory, so run from a dedicated empty one. Combined with
|
# the working directory, so run from a dedicated empty one. Combined with
|
||||||
# --no-session-persistence and --safe-mode, a launcher question leaves no
|
# --no-session-persistence and --safe-mode, a launcher question leaves no
|
||||||
# trace in the real projects' history or memory.
|
# trace in the real projects' history or memory.
|
||||||
work="$HOME/.cache/qs-ask"
|
work="$HOME/.cache/qs-ask"
|
||||||
${pkgs.coreutils}/bin/mkdir -p "$work" || { echo "qs-ask: cannot create $work" >&2; exit 0; }
|
${pkgs.coreutils}/bin/mkdir -p "$work" || { log "cannot create $work"; exit 0; }
|
||||||
cd "$work" || { echo "qs-ask: cannot enter $work" >&2; exit 0; }
|
cd "$work" || { log "cannot enter $work"; exit 0; }
|
||||||
|
|
||||||
# MAX_THINKING_TOKENS=0 is the single biggest win here, worth ~2.4x.
|
# MAX_THINKING_TOKENS=0 is the single biggest win here, worth ~2.4x.
|
||||||
# Measured: Haiku defaults to extended thinking, and it spent 225 output
|
# Measured: Haiku defaults to extended thinking, and it spent 225 output
|
||||||
|
|
@ -142,14 +156,15 @@
|
||||||
--name qs-ask \
|
--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.' \
|
--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) \
|
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:]]*$//')
|
ans=$(printf '%s' "$ans" | ${pkgs.coreutils}/bin/tr -d '\r' | ${pkgs.gnused}/bin/sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||||
|
|
||||||
case "$ans" in
|
case "$ans" in
|
||||||
"") echo "qs-ask: empty answer" >&2; exit 0 ;;
|
"") log "empty answer"; exit 0 ;;
|
||||||
UNKNOWN*) echo "qs-ask: model replied UNKNOWN" >&2; exit 0 ;;
|
UNKNOWN*) log "model replied UNKNOWN"; exit 0 ;;
|
||||||
esac
|
esac
|
||||||
|
log "ok: $ans"
|
||||||
printf '%s' "$ans"
|
printf '%s' "$ans"
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -2415,7 +2415,10 @@ in
|
||||||
height: answerCol.height + 28
|
height: answerCol.height + 28
|
||||||
radius: Theme.radiusCard
|
radius: Theme.radiusCard
|
||||||
color: Theme.cardBg
|
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 {
|
Column {
|
||||||
id: answerCol
|
id: answerCol
|
||||||
|
|
@ -2451,6 +2454,7 @@ in
|
||||||
// answer already came from it.
|
// answer already came from it.
|
||||||
SText {
|
SText {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
visible: launcherPanel.answerFact !== ""
|
||||||
text: launcherPanel.answerFact
|
text: launcherPanel.answerFact
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
@ -2466,13 +2470,38 @@ in
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always say who answered — the two sources
|
// 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 {
|
SText {
|
||||||
text: launcherPanel.answerSource
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: askProc.running
|
||||||
|
? (launcherPanel.answerSource === "" ? "Claude" : launcherPanel.answerSource + " · Claude")
|
||||||
|
: launcherPanel.answerSource
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10
|
||||||
color: Theme.base03
|
color: Theme.base03
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only Wikipedia answers have somewhere to go.
|
// Only Wikipedia answers have somewhere to go.
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue