diff --git a/CHANGELOG.md b/CHANGELOG.md index 28cd3c3..6afd5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,30 @@ and this project adheres to up with two skills claiming the same job. The asset probe accepts a pre-rename checkout named via `SINTERACTIVE_SHARE`. +- The notice lines below the status bar are gone. Everything the session has + to say about itself is collapsed into a compact `⚠ N notices` counter at + the right of the session line — red while the quota warning is among them, + yellow otherwise, absent when there is nothing to say — so the panel is + always exactly one status line tall. A new `Ctrl-b n` popup shows the + notices in full (`Ctrl-b n` overrides tmux's stock next-window, which has + nothing to do here with the window list hidden), and `sinteractive + --status` prints the same text from the login node, reading the notices + file the session maintains in `~/.cache/sinteractive/`. + + While the quota warning holds, the counter shimmers — a lighter band + sweeping through the red text, the way Claude Code's spinner verbs do — + so the one notice that needs acting on keeps catching the eye without + taking any more room than the others. + + The warnings held a row of pane height for their whole life — quota and a + maintenance-trimmed end time never clear on their own — and every + appearance resized every pane in the session. A counter does neither, at + the price of one keypress to read the text; and with the width limit gone, + the maintenance notice can afford to say what it means again. + +- The `Detach: Ctrl+b d` hint is out of the status line; it already lives in + the `Ctrl-b h` help popup, which now also lists `Ctrl-b n`. + ## [0.6.0] - 2026-08-26 ### Changed diff --git a/docs/scripting.md b/docs/scripting.md index 745be4e..ec91c27 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -215,10 +215,11 @@ state file and only falls back to the scheduler when it is stale, so a quiet session costs nothing. While Claude Code is running in a session whose hooks are not registered yet, -a `sinteractive --install-claude` hint sits on the bottom status line, below -the session line and below any warnings. It is gated on a live `claude` -process, so it never appears for people who don't use Claude Code, and it -clears once the hooks are registered — taking its line with it. +a `sinteractive --install-claude` hint joins the session's notices — the +`⚠ N notices` counter on the status line, read in full with `Ctrl-b n` or +`sinteractive --status`. It is gated on a live `claude` process, so it never +appears for people who don't use Claude Code, and it clears once the hooks +are registered. Hooks fire at turn and tool boundaries, so work already in flight cannot be warned about — put long work in its own allocation, which outlives the diff --git a/docs/usage.md b/docs/usage.md index ab5bfa0..00c8802 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -186,6 +186,7 @@ status bar, `squeue`, `--list`, and works with `--attach NAME`. | Action | Key | |---|---| | Show help popup (job info, keys) | `Ctrl-b h` | +| Show notices popup (quota, maintenance, hints) | `Ctrl-b n` | | Detach from session | `Ctrl-b d` | | Name/rename session (updates squeue and `--attach` name) | `Ctrl-b $` | | Split pane horizontally | `Ctrl-b "` | diff --git a/man/sinteractive.1 b/man/sinteractive.1 index aff453b..b93689d 100644 --- a/man/sinteractive.1 +++ b/man/sinteractive.1 @@ -190,7 +190,8 @@ elapsed time, time limit, and the session's current working directory. .TP .BI \-\-status\ [TARGET] Show session status: state, node, partition, allocated CPUs and memory, -elapsed time, and time remaining. +elapsed time, time remaining, and the session's active notices (see +.BR "STATUS BAR" ). .I TARGET is a .I JOBID @@ -270,10 +271,13 @@ read at all. .IP Sessions poll this on their own every .B $SINTERACTIVE_QUOTA_POLL -seconds (default 600) and show a red +seconds (default 600) and, while it holds, carry a red .B QUOTA -notice at the left of a second status line, below the session line, while it -holds. It reports the overage, not the usage \(em the number to act on. +notice behind the notices counter on the session line, read in full with +.B Ctrl\-b n +(see +.BR "STATUS BAR" ). +It reports the overage, not the usage \(em the number to act on. Running .B \-\-check\-quota after freeing space clears that notice within a tick rather than at the end @@ -400,6 +404,13 @@ reattach commands, and basic tmux keys. Any key closes it. Detach from the session; the job keeps running. Reattach later with .BR "sinteractive \-\-attach" . .TP +.B Ctrl\-b n +Open the notices popup: the full text behind the +.RB \(lq "N notices" \(rq +counter on the status line (see +.BR "STATUS BAR" ). +Any key closes it. +.TP .B Ctrl\-b $ Name (or rename) the session in place. Updates the squeue job name and the name used by @@ -442,19 +453,22 @@ About ten seconds before the limit, a notice is printed in every pane and the session shuts down cleanly, so the job completes instead of being killed at timeout. .PP -Nothing else is crowded into that line. Anything the session has to say -about itself appears below it, on status lines of its own, each present -only while it has something to say and costing a row of pane height only -while it is there. Nearest the session line are the warnings: +Anything else the session has to say about itself is collapsed into a +compact counter at the right of that same line \(em +.RB \(lq "N notices" \(rq +\(em red and shimmering while the quota warning is among them, yellow +otherwise, and absent while there is nothing to say. Press +.B Ctrl\-b n +to read the notices in full; +.B sinteractive \-\-status +prints the same text from the login node. Today's notices are the .B QUOTA -in red at the left of the line (see +warning (see .BR \-\-check\-quota ), -and at its right, in yellow, the end time when the request was trimmed to -finish before a maintenance reservation. Both hold for the life of the -session, so when both apply they share the line, one at each end of it \(em -and each keeps its side whether or not the other is there. Below them, furthest from the session -line because it is only an offer, is the Claude Code hint (see +the end time when the request was trimmed to finish before a maintenance +reservation, and the Claude Code hint (see .BR "CLAUDE CODE INTEGRATION" ). +The panel is always exactly one status line tall. .PP The rule between the pane and the status bar carries no text at all. .SH SCRIPTING AND AGENTS @@ -559,12 +573,14 @@ is available; see .BR \-\-install\-claude . .PP While Claude Code is running in a session whose integration is not yet -registered, a yellow +registered, a .B "sinteractive \-\-install\-claude" -hint sits on the bottom status line, below any warnings \(em furthest -from the session line, because it is an offer rather than something to -act on. It is a whole line and fits an 80-column terminal, so it is -read at a glance rather than scrolled past. It appears only while a +hint joins the session's notices (the counter on the status line, read +with +.BR Ctrl\-b n ; +see +.BR "STATUS BAR" ). +It appears only while a .B claude process is actually running, and clears once the hooks are registered, so it never nags anyone who does not use Claude Code. diff --git a/sinteractive b/sinteractive index 78b3ded..2482a22 100755 --- a/sinteractive +++ b/sinteractive @@ -160,7 +160,7 @@ ${C_HDR}Examples:${C_RST} sinteractive --attach ${C_DIM}# reconnect to your only session${C_RST} sinteractive --cancel myproj ${C_DIM}# cancel a session by name${C_RST} -Inside the session, ${C_KEY}Ctrl-b h${C_RST} opens a help popup; ${C_KEY}Ctrl-b d${C_RST} detaches +Inside the session, ${C_KEY}Ctrl-b h${C_RST} opens a help popup, ${C_KEY}Ctrl-b n${C_RST} the notices; ${C_KEY}Ctrl-b d${C_RST} detaches (the job keeps running). ${C_WARN}A session is for orchestration, not compute:${C_RST} it is usually a small @@ -189,6 +189,10 @@ function main { shift show_help_popup "$@" return + elif [[ "${1:-}" == 'notices-popup' ]]; then + shift + show_notices_popup "$@" + return elif [[ "${1:-}" == 'rename-session' ]]; then shift rename_session "$@" @@ -1104,6 +1108,20 @@ function show_status { printf " ${C_KEY}%-11s${C_RST} ${rem_c}%s${C_RST}\n" \ 'Remaining:' "$(format_short_duration "$remaining")" fi + # The session's active notices — the full text behind the "⚠ N notices" + # indicator on its status line, readable without attaching. The file is + # maintained by status_dot_loop on the compute node and carried here by + # the shared home filesystem; absent file, nothing to say. + local notices_file="${HOME}/.cache/sinteractive/${jobid}.notices" + if [[ -s "$notices_file" ]]; then + local n_kind n_text n_c + while IFS=$'\t' read -r n_kind n_text; do + [[ -n "$n_text" ]] || continue + n_c="${C_WARN}" + [[ "$n_kind" == quota ]] && n_c="${C_ERR}${C_BOLD}" + printf " ${C_KEY}%-11s${C_RST} ${n_c}%s${C_RST}\n" 'Notice:' "$n_text" + done <"$notices_file" + fi fi exit 0 } @@ -1204,7 +1222,8 @@ Re-check this session with \`sinteractive --status --json\` before long work; the number above was read when this briefing was generated, and a walltime can be changed underneath you. ${quota_line} -Storage quota is shown on a line below the status bar while it is exceeded. +Storage quota, while exceeded, is a red notice behind the "N notices" counter +on the status line (shown in full by \`sinteractive --status\`). Check it with \`sinteractive --check-quota\`, and run that again after deleting anything on the user's behalf — it refreshes every open session, so the warning clears immediately rather than up to ten minutes later. @@ -1398,9 +1417,9 @@ function quota_cached { }' "$file" } -# Poke every live session so a fresh probe reaches their notice lines on the -# next tick rather than at the end of the ten-minute poll. This is what makes -# `--check-quota` feel immediate after deleting something. +# Poke every live session so a fresh probe reaches their notices indicator on +# the next tick rather than at the end of the ten-minute poll. This is what +# makes `--check-quota` feel immediate after deleting something. function poke_all_sessions { local dir="${HOME}/.cache/sinteractive" f jobid [[ -d "$dir" ]] || return 0 @@ -1442,8 +1461,9 @@ function check_quota_cmd { local over used hard over_kb pct read -r over used hard over_kb pct _ < <(quota_cached) init_colors 1 - # Wording matched to the status-bar notice and quoted verbatim in - # skills/hpc-storage/SKILL.md — colour is the only thing added here. + # Wording matched to the quota notice (see quota_notice_string) and quoted + # verbatim in skills/hpc-storage/bodhi.md — colour is the only thing + # added here. if ((over)); then printf "${C_ERR}${C_BOLD}OVER QUOTA:${C_RST}${C_ERR} %s of %s used (%s%%), over by %s${C_RST}\n" \ "$(quota_kb_to_size "$used")" "$(quota_kb_to_size "$hard")" \ @@ -1455,99 +1475,106 @@ function check_quota_cmd { exit 0 } -# Anything the session has to say about itself goes below the status bar, on -# tmux status lines of its own — never squeezed into the session line or onto -# the rule above it. There are two, each present only while it has something -# to say, and each costing a row of pane height only while it is there: +# Anything the session has to say about itself is collapsed into one compact +# indicator at the right of the session line — "⚠ N notices Ctrl+b n" — red +# while the quota warning is among them, yellow otherwise. The notices are +# read in full in the Ctrl+b n popup, or from the login node with --status. +# The panel is always exactly one status line tall. # -# 1. warnings — quota (red, flush left) and a maintenance-trimmed end time -# (yellow, flush right). Both hold for the whole session, so they share -# their line rather than taking turns, one at each end of it. -# 2. the Claude Code hint (yellow) — purely an offer, so it sits furthest -# from the session line and is the first thing to go. +# The notices used to be status lines of their own below the session line +# (and before that, a marquee on the pane border). Each held a row of pane +# height for its whole life — the quota and maintenance warnings never clear +# on their own — and every appearance resized every pane in the session. A +# counter does neither, at the price of one keypress to read the text; and +# with the width limit gone, the text itself can afford to say more. +# +# Each producer prints one line for the notices file — "kindtext", plain +# text — or returns 1 when it has nothing to say. Colour belongs to the +# renderers (the indicator, the popup, --status), keyed off the kind, so the +# file itself stays greppable. # -# All of it used to live on the pane border, one line carrying three -# unrelated things and, for the hint, scrolling them past. Full-width lines -# of their own need no marquee: the hint fits a normal terminal whole, which -# also drops the 0.3s redraw the scroll cost every session that showed it. # The overage, not the usage: it is the number you act on, and "over by X" -# says on its own that you are over — so the word OVER, the used figure and -# the percentage all come out and the line still says the same thing. +# says on its own that you are over — so the used figure and the percentage +# stay out and the line still says the same thing. function quota_notice_string { local over="$1" hard="$2" over_kb="$3" ((over)) || return 1 - printf '#[fg=red,bold] ⚠ QUOTA over by %s (%s limit) #[default]' \ + printf 'quota\tQUOTA over by %s (%s limit)' \ "$(quota_kb_to_size "$over_kb")" "$(quota_kb_to_size "$hard")" } -# No SHORT SESSION label: sitting on the warnings line, in yellow, opposite -# the quota warning, an end time that is not the one you asked for is already -# reading as a warning. The reservation name is what you would look up. +# The reservation name is what you would look up in sinfo/scontrol. function maint_notice_string { local name="$1" start_epoch="$2" [[ -n "$start_epoch" ]] || return 1 - printf '#[fg=yellow,bold] ends %s · %s #[default]' \ - "$(date -d "@$start_epoch" '+%a %H:%M' 2>/dev/null)" "$name" + printf 'maint\tSession ends %s — trimmed to finish before maintenance (%s)' \ + "$(date -d "@$start_epoch" '+%a %b %-d %H:%M' 2>/dev/null)" "$name" } -# The hint is one line whether or not it fits: at a narrow width tmux cuts -# the tail, and the command — the only part that has to be read — is early -# enough to survive that. function hint_notice_string { local wanted="$1" ((wanted)) || return 1 - printf '#[fg=yellow] Claude Code: run #[bold]sinteractive --install-claude#[nobold] to enable the skills and hooks #[default]' + printf 'hint\tClaude Code: run sinteractive --install-claude to enable the skills and hooks' } -# Quota flush left, maintenance flush right — the same two ends the session -# line above already uses, so the notices sit under something rather than -# starting in the middle of the line. Each half keeps its side whether or not -# the other is there, so a notice does not move when its neighbour appears. -# -# tmux resolves each #[align=...] against the whole line, which is how the -# stock status-format[0] gets status-left and status-right onto one row; the -# separator the two used to share is what the gap between them replaces. Every -# part already carries its own padding, so neither touches the screen edge. -function compose_notice { - local quota_part="$1" maint_part="$2" - [[ -n "$quota_part" ]] && printf '%s' "#[align=left]${quota_part}" - [[ -n "$maint_part" ]] && printf '%s' "#[align=right]${maint_part}" - return 0 +# The notices file is what the Ctrl+b n popup and --status read; the shared +# home filesystem is what carries it to the login node. Removed when the last +# notice clears (and at session teardown), so its absence means "nothing to +# say". Written whole via rename so a popup opened mid-write never reads half +# a file. +function write_notices_file { + local jobid="$1" + shift + local file="${HOME}/.cache/sinteractive/${jobid}.notices" + if (($#)); then + printf '%s\n' "$@" >"${file}.tmp" 2>/dev/null && + mv -f "${file}.tmp" "$file" 2>/dev/null + else + rm -f "$file" "${file}.tmp" + fi } -# Put the given lines below the status bar, in order, and size the status bar -# to hold them. Callers pass only the lines that have something on them, so -# the hint moves up to line 1 when there is no warning and there is never a -# blank row between the session line and a notice. -# -# Formats are set before the status bar grows, so tmux never draws a frame of -# its own default line 1 (the window list) on the way in. Shrinking leaves -# the stale formats in place: they are not rendered, and clearing them would -# cost a round trip each. +# Right side of the session line. The stock form is just the help hint — the +# detach key lives in the help popup, not here. With notices, the counter +# joins it, red when the quota warning is present and yellow otherwise. +# start_tmux seeds status-right from this same function, so the loop's +# notice-free form and the launch-time form cannot drift apart. # -# status-format is set globally and status per session, which looks -# inconsistent and is not. Setting one element of an option array at session -# scope replaces the whole array for that session rather than overlaying the -# global one, so a session-scoped status-format[1] leaves status-format[0] -# empty — and the session line, the thing these are meant to sit under, -# silently disappears. Setting the extra indices globally leaves the default -# index 0 alone. (Every job has its own tmux server, so global here is still -# one session's worth of scope.) -function apply_notice_lines { - local sock="$1" session_name="$2" - shift 2 - local lines=("$@") i - for i in "${!lines[@]}"; do - $TMUX_BIN -L "$sock" set-option -g "status-format[$((i + 1))]" \ - "${lines[i]}" 2>/dev/null - done - # tmux spells a one-line status bar "on" and taller ones as a count. - local height=on - ((${#lines[@]})) && height=$((${#lines[@]} + 1)) - $TMUX_BIN -L "$sock" set-option -t "$session_name" status "$height" 2>/dev/null +# While the quota warning is up, the counter shimmers: a lighter band sweeps +# through the red text, one character per frame, the way Claude Code's +# spinner verbs do — the one warning that needs acting on keeps catching the +# eye without taking any more room. The frame is the caller's tick counter +# (status_dot_loop shortens its sleep to animate it); static callers pass 0. +# The band walks only the ASCII "N notices" part — bash slices by characters +# only when the locale agrees with the bytes, and the ⚠ is not worth betting +# on that. +function status_right_string { + local count="$1" severe="$2" frame="${3:-0}" + local right=' Help: #[fg=yellow,bold]Ctrl+b h#[default] ' + if ((count > 0)); then + local noun=notices + ((count == 1)) && noun=notice + if ((severe)); then + local text="$count $noun" band=3 + local len=${#text} + # Band start runs -band..len-1, so the highlight slides in from the + # left edge and off the right one instead of popping. + local a=$((frame % (len + band) - band)) b + b=$((a + band)) + ((a < 0)) && a=0 + ((b > len)) && b=$len + ((b < a)) && b=$a + printf ' #[fg=red,bold]⚠ %s#[fg=colour217]%s#[fg=red]%s#[default] #[fg=yellow,bold]Ctrl+b n#[default] |' \ + "${text:0:a}" "${text:a:b-a}" "${text:b}" + else + printf ' #[fg=yellow,bold]⚠ %d %s#[default] #[fg=yellow,bold]Ctrl+b n#[default] |' \ + "$count" "$noun" + fi + fi + printf '%s' "$right" } -# Whether to nag about --install-claude in the status bar: only while Claude +# Whether to nag about --install-claude in the notices: only while Claude # Code is actually running in this session and the integration is not live. # # The trigger is a running process, not an installed binary or an existing @@ -1989,8 +2016,8 @@ MAINT_MARGIN=300 # more — so a session asked for at the default day length simply stops working # as maintenance approaches, with no obvious reason. # -# Now the request is trimmed to fit and the session says so, in the notice -# line and at launch. Echoes "fitted_secs name start_epoch original_time" when +# Now the request is trimmed to fit and the session says so, in its notices +# and at launch. Echoes "fitted_secs name start_epoch original_time" when # it trimmed, nothing when there was no conflict. Returns 1 only when the # remaining gap is too small to be worth a session at all. function fit_maintenance_window { @@ -2268,6 +2295,7 @@ function show_help_popup { ${hdr}Session${rst} Detach (keeps job running) ${key}Ctrl+b d${rst} + View active notices ${key}Ctrl+b n${rst} Name/rename session ${key}Ctrl+b \$${rst} Reattach from login node ${key}sinteractive --attach ${jobid:-JOBID}${rst} List your sessions ${key}sinteractive --list${rst} @@ -2292,6 +2320,38 @@ EOF read -rsn1 } +# Render the in-session notices modal: the full text behind the "⚠ N notices" +# indicator. Reads the notices file status_dot_loop maintains (rewritten on +# every quota re-read and on poke, so the text is as fresh as the indicator). +# Runs inside a tmux display-popup on the compute node, invoked as +# "notices-popup JOBID" via the spooled batch script — same shape as the help +# popup above. +function show_notices_popup { + local jobid="${1:-${SLURM_JOB_ID:-}}" + local file="${HOME}/.cache/sinteractive/${jobid}.notices" + + local rst=$'\e[0m' dim=$'\e[2m' hdr=$'\e[1;36m' + local red=$'\e[1;31m' yellow=$'\e[33m' + + echo + echo " ${hdr}Notices${rst} ${dim}for job ${jobid:-?}${rst}" + echo + local shown=0 kind text colour + if [[ -s "$file" ]]; then + while IFS=$'\t' read -r kind text; do + [[ -n "$text" ]] || continue + colour="$yellow" + [[ "$kind" == quota ]] && colour="$red" + printf ' %s⚠ %s%s\n' "$colour" "$text" "$rst" + shown=1 + done <"$file" + fi + ((shown)) || echo " ${dim}No active notices.${rst}" + echo + echo " ${dim}Press any key to close.${rst}" + read -rsn1 +} + # Flash a message in the status line of every attached client. Handlers run # via run-shell with no client context, so target clients explicitly; with no # clients attached (detached session) this is a silent no-op. @@ -2454,7 +2514,7 @@ function status_dot_loop { local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local end_epoch='' end_query=0 end_checked=0 spin=0 belled=0 last='' - local hint_shown=0 hint_checked=0 last_notice='' + local hint_shown=0 hint_checked=0 last_notice='' last_right='' shimmer=0 local quota_checked=0 quota_part='' maint_part='' maint_part=$(maint_notice_string "$maint_name" "$maint_start") || maint_part='' while $TMUX_BIN -L "$sock" has-session -t "$session_name" 2>/dev/null; do @@ -2507,21 +2567,32 @@ function status_dot_loop { claude_hint_wanted && hint_shown=1 fi - # The lines below the status bar, applied here rather than further down + # The notices indicator and file, applied here rather than further down # so they are up on the first tick and stay up on a job whose end time # Slurm will not report (the no-deadline `continue` below). Only touched - # when the text changes: growing and shrinking the status bar resizes - # every pane in the session, so a steady state must not keep re-setting - # it. - local notice_lines=() line - line=$(compose_notice "$quota_part" "$maint_part") - [[ -n "$line" ]] && notice_lines+=("$line") + # when the set changes: a steady state must not keep re-setting tmux + # options or rewriting the file. + local notice_lines=() line severe=0 + [[ -n "$quota_part" ]] && notice_lines+=("$quota_part") && severe=1 + [[ -n "$maint_part" ]] && notice_lines+=("$maint_part") line=$(hint_notice_string "$hint_shown") && notice_lines+=("$line") local notice="${notice_lines[*]}" if [[ "$notice" != "$last_notice" ]]; then - apply_notice_lines "$sock" "$session_name" "${notice_lines[@]}" + write_notices_file "$SLURM_JOB_ID" "${notice_lines[@]}" last_notice="$notice" fi + # The indicator is compared against what is already up rather than + # against the notice set: the shimmer makes it a new string every frame + # while the quota warning holds, and a steady state without one must not + # keep re-setting the option. + local right + right=$(status_right_string "${#notice_lines[@]}" "$severe" "$shimmer") + if [[ "$right" != "$last_right" ]]; then + $TMUX_BIN -L "$sock" set-option -t "$session_name" status-right \ + "$right" 2>/dev/null + last_right="$right" + fi + ((severe)) && shimmer=$((shimmer + 1)) # One cadence for both halves of the state file: ask Slurm for the end # time immediately before each write, so a `scontrol update TimeLimit` @@ -2532,9 +2603,14 @@ function status_dot_loop { refresh_end_epoch "$now" fi # No parseable end time yet (UNLIMITED, squeue hiccup): leave the white - # dot alone and try again later. + # dot alone and try again later — at shimmer cadence while the quota + # warning is animating, since this branch skips the sleep at the bottom. if [[ -z "$end_epoch" ]]; then - sleep 60 + if ((severe)); then + sleep 0.2 + else + sleep 60 + fi continue fi @@ -2584,13 +2660,14 @@ function status_dot_loop { belled=0 elif ((remaining > warn_red)); then # Pulse the dot bold/dim once a second — catches the eye without the - # urgency of the red phase's spinner. - if ((spin % 2)); then + # urgency of the red phase's spinner. Keyed to the wall clock, not the + # tick count, so the beat stays one second even when the quota shimmer + # below shortens the tick. + if ((now % 2)); then glyph='#[fg=yellow,dim]●#[default]' else glyph='#[fg=yellow,bold]●#[default]' fi - spin=$((spin + 1)) text=" #[dim]| $(format_short_duration "$remaining") left#[default]" sleep_for=1 belled=0 @@ -2657,10 +2734,17 @@ function status_dot_loop { $TMUX_BIN -L "$sock" set-option -t "$session_name" status-left "$left" last="$left" fi + # The shimmer needs frames: while the quota warning is up, tick fast + # enough to animate it. The final countdown is already at 10Hz; every + # slower phase drops to 0.2s, which sweeps the counter in about two + # seconds. Everything else in the loop is gated on wall-clock deltas, so + # the faster tick costs only the frame itself. + ((severe)) && [[ "$sleep_for" != 0.1 ]] && sleep_for=0.2 sleep "$sleep_for" done - rm -f "$state_file" "${state_file}.tmp" "${state_file%.json}.poke" + rm -f "$state_file" "${state_file}.tmp" "${state_file%.json}.poke" \ + "${state_file%.json}.notices" "${state_file%.json}.notices.tmp" } function start_tmux { @@ -2770,8 +2854,10 @@ function start_tmux { # time-remaining clock near the limit — see status_dot_loop, which keeps # this string updated) and the job ID + host in bold cyan (#2DBFB8, the # teal-cyan rustc/cargo use for ANSI cyan), with the label and separator - # dimmed. Right = hint for the help popup; the full help (reattach command, - # docs link, tmux keys) lives in the Ctrl+b h modal. + # dimmed. Right = hint for the help popup, joined by the "⚠ N notices" + # counter while there are notices (see status_right_string); the full help + # (reattach and detach commands, docs link, tmux keys) lives in the Ctrl+b h + # modal and the notice text in the Ctrl+b n one. # The trailing conditional appends "| " when the session has a name. # tmux format lookup falls back to the session environment, so the segment # tracks SINTERACTIVE_NAME live (set at launch with --name, or later via @@ -2787,7 +2873,7 @@ function start_tmux { $TMUX_BIN -L "$sock" set-option -t "$session_name" window-status-format '' $TMUX_BIN -L "$sock" set-option -t "$session_name" window-status-current-format '' $TMUX_BIN -L "$sock" set-option -t "$session_name" status-right \ - " Help: #[fg=yellow,bold]Ctrl+b h#[default] | Detach: #[fg=yellow,bold]Ctrl+b d#[default] " + "$(status_right_string 0 0)" $TMUX_BIN -L "$sock" set-option -t "$session_name" status-right-length 40 # Terminal window/tab title (OSC 2): mirror the status-left text so users # with several sessions can tell tabs apart at a glance. @@ -2802,8 +2888,8 @@ function start_tmux { # windows created later get it too. # # It stays empty for the life of the session. Everything the session has to - # say goes below the status bar instead (see apply_notice_lines), where a - # message gets a whole line rather than a gap cut into the rule. + # say is behind the "⚠ N notices" indicator on the status line instead (see + # status_right_string), read in full via the Ctrl+b n popup. $TMUX_BIN -L "$sock" set-option -g pane-border-status bottom $TMUX_BIN -L "$sock" set-option -g pane-border-format '' $TMUX_BIN -L "$sock" set-option -g pane-border-lines heavy @@ -2819,6 +2905,14 @@ function start_tmux { -T ' sinteractive help ' \ "'${BASH_SOURCE[0]}' help-popup ${SLURM_JOB_ID}" + # Notices popup: prefix + n shows the full text behind the "⚠ N notices" + # indicator on the status line (see status_right_string). This overrides + # tmux's stock n (next-window) deliberately: the window list is hidden from + # the status bar, so next-window has nothing here for n to be about. + $TMUX_BIN -L "$sock" bind-key n display-popup -E -w 76 -h 12 \ + -T ' notices ' \ + "'${BASH_SOURCE[0]}' notices-popup ${SLURM_JOB_ID}" + # Name/rename the session in place: prefix + $ prompts for a name and # hands it to the rename-session handler (scontrol Comment/Name update, # SINTERACTIVE_NAME env). This deliberately overrides tmux's stock $ @@ -2852,7 +2946,9 @@ function start_tmux { # KillWait); remove the state file on the way out so it can't go stale. trap 'rm -f "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.json" \ "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.json.tmp" \ - "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.poke"; exit 0' TERM + "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.poke" \ + "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.notices" \ + "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.notices.tmp"; exit 0' TERM # Run wait-for in background; `wait` builtin (foreground) is interruptible # by signals, unlike external commands which block trap delivery in bash. @@ -2870,7 +2966,9 @@ function start_tmux { # session die, but Slurm's cgroup cleanup can kill it mid-sleep first. rm -f "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.json" \ "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.json.tmp" \ - "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.poke" + "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.poke" \ + "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.notices" \ + "${HOME}/.cache/sinteractive/${SLURM_JOB_ID}.notices.tmp" trap - USR1 } diff --git a/skills/hpc-storage/bodhi.md b/skills/hpc-storage/bodhi.md index 2901e7f..b279614 100644 --- a/skills/hpc-storage/bodhi.md +++ b/skills/hpc-storage/bodhi.md @@ -71,8 +71,9 @@ sinteractive --check-quota sinteractive --check-quota --json # same, machine-readable ``` -A session shows a red `⚠ QUOTA over by …` notice at the left of a line below -its status bar while this holds, refreshed every ten minutes. +While this holds, a session carries a red `QUOTA over by …` notice behind the +`⚠ N notices` counter on its status line (read in full with `Ctrl-b n`, or via +`sinteractive --status`), refreshed every ten minutes. **After deleting anything on the user's behalf, run `--check-quota`.** It re-checks immediately and pushes the result to every open session, so the