diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe462a..a8f42c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,37 @@ and this project adheres to ## [Unreleased] +## [0.5.1] - 2026-08-26 + +### Changed + +- Everything the session has to say about itself moved below the status bar, + onto lines of its own. The pane border was carrying three unrelated things + at once — `OVER QUOTA`, `SHORT SESSION`, and a scrolling offer to install + the Claude Code hooks — sharing one rule and taking turns for it. The line + you glance at for the job id was busy enough to stop reading. + + There are now up to two lines under the session line, each present only + while it has something on it: warnings nearest (the two share a line, since + both hold for the whole session), then the Claude Code hint, furthest away + because it is an offer rather than something to act on. The pane border is + back to being a plain rule with no text. The status bar grows and shrinks + with the notices, so a session with nothing to say is exactly as tall as it + was before. + + The hint no longer scrolls. It scrolled because a 44-column window was all + the border could give it without swallowing the rule; a full-width line of + its own fits the whole sentence in an 80-column terminal. That also retires + the 0.3s redraw the marquee forced on every session that showed it. + + One tmux subtlety, found by rendering it rather than by reading: setting one + element of an option array at session scope replaces the whole array for + that session instead of overlaying the global one, so a session-scoped + `status-format[1]` leaves `status-format[0]` empty and the session line — + the thing the notices are meant to sit under — silently disappears. The + extra indices are set globally instead, which leaves the default index 0 + alone. + ## [0.5.0] - 2026-08-26 ### Added @@ -471,7 +502,8 @@ First tagged release. installers (user, system-wide, and per-node fan-out), and a `bodhi-compute` Claude Code skill. -[Unreleased]: https://github.com/rnabioco/sinteractive/compare/v0.5.0...HEAD +[Unreleased]: https://github.com/rnabioco/sinteractive/compare/v0.5.1...HEAD +[0.5.1]: https://github.com/rnabioco/sinteractive/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/rnabioco/sinteractive/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/rnabioco/sinteractive/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/rnabioco/sinteractive/compare/v0.2.2...v0.3.0 diff --git a/README.md b/README.md index 741a154..c4ff13f 100644 --- a/README.md +++ b/README.md @@ -184,19 +184,22 @@ sequenceDiagram Note over C: you work here ``` -## The notice line +## Notice lines -The rule between your pane and the status bar is normally empty. When there is -something you should know, it carries it — full width, so a message is -readable at a glance rather than crammed next to the clock. - -Three things can appear there, ranked, because something you must act on -outranks something you should know: +When something about the session needs saying, it gets a line of its own under +the status bar rather than a corner of one that is already busy. Each line is +there only while its message is, and takes a row of pane height while it is: ``` -━━━ ⚠ OVER QUOTA 30.2T / 30T · over by 204.8G │ SHORT SESSION · ends before monthly-maint at Thu 06:00 ━━━ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ● session 245772 | compute07 Help: Ctrl+b h | Detach: Ctrl+b d + ⚠ OVER QUOTA 30.2T / 30T · over by 204.8G │ SHORT SESSION · ends before monthly-maint at Thu 06:00 + Claude Code: run sinteractive --install-claude to enable the skills and hooks ``` +Nearest the session line are the warnings. Both stay true for the whole +session, so they share their line rather than taking turns. + **Over quota** (red). Checked every 10 minutes against the cluster's quota daemons. The check is cached per user, not per session, so having six sessions open does not mean six times the polling. @@ -208,13 +211,17 @@ sinteractive --check-quota # re-checks now, updates every open session ``` That is also the command to hand an agent — it clears the warning within a -tick of the space actually being freed. +tick of the space actually being freed, and the line goes away with it. **Short session** (yellow). Shown when the request was trimmed to end before a maintenance window — see below. -**Claude Code hint** (yellow, scrolling). Only when nothing more important -wants the line, and only while `claude` is actually running in the session. +**Claude Code hint** (yellow). Bottom line, furthest from the session line +because it is an offer rather than something to act on. Shown only while +`claude` is running in a session where the hooks are not registered, and gone +once they are. + +The rule between your pane and the status bar carries no text at all. ## Maintenance windows diff --git a/docs/deploy.md b/docs/deploy.md index a297c69..4a3e3dc 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -67,7 +67,7 @@ script there, so a copy may be executing while you install. ```bash make nodes-check -# compute00 sinteractive=0.5.0 assets=yes tmux 3.7c +# compute00 sinteractive=0.5.1 assets=yes tmux 3.7c # compute01 sinteractive=unknown assets=no tmux 3.7b # compgpu01 unreachable ``` diff --git a/docs/scripting.md b/docs/scripting.md index 97f44c3..934eae4 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -207,10 +207,10 @@ 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, -the yellow rule between the pane and the status bar carries a centred, -scrolling `sinteractive --install-claude` notice. 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. +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. 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/man/sinteractive.1 b/man/sinteractive.1 index c5d6da2..348607e 100644 --- a/man/sinteractive.1 +++ b/man/sinteractive.1 @@ -1,4 +1,4 @@ -.TH SINTERACTIVE 1 "August 2026" "sinteractive 0.5.0" "User Commands" +.TH SINTERACTIVE 1 "August 2026" "sinteractive 0.5.1" "User Commands" .SH NAME sinteractive \- start an interactive tmux session on a Slurm compute node .SH SYNOPSIS @@ -272,7 +272,8 @@ Sessions poll this on their own every .B $SINTERACTIVE_QUOTA_POLL seconds (default 600) and show a red .B OVER QUOTA -notice above the status line while it holds. Running +notice on a second status line, below the session line, while it holds. +Running .B \-\-check\-quota after freeing space clears that notice within a tick rather than at the end of the poll interval, which is what makes it useful to an agent that has just @@ -439,6 +440,23 @@ terminal tab is flagged even when you are looking elsewhere. 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: +.B "OVER QUOTA" +in red (see +.BR \-\-check\-quota ) +and +.B "SHORT SESSION" +in yellow, the latter when the request was trimmed to end before a +maintenance reservation. Both hold for the life of the session, so when +both apply they share the line. Below them, furthest from the session +line because it is only an offer, is the Claude Code hint (see +.BR "CLAUDE CODE INTEGRATION" ). +.PP +The rule between the pane and the status bar carries no text at all. .SH SCRIPTING AND AGENTS Headless workflows (e.g. Claude Code) can launch and inspect sessions without attaching: @@ -541,13 +559,12 @@ is available; see .BR \-\-install\-claude . .PP While Claude Code is running in a session whose integration is not yet -registered, the yellow rule between the pane and the status bar carries a -centred, scrolling +registered, a yellow .B "sinteractive \-\-install\-claude" -notice. The rule is full width and otherwise empty, so this costs nothing -in the status line itself; the notice scrolls through a fixed 44-column -window at about three columns a second, so it reads the same at any -terminal width. It appears only while a +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 .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/pixi.toml b/pixi.toml index 35dc1b2..a744425 100644 --- a/pixi.toml +++ b/pixi.toml @@ -1,7 +1,7 @@ [workspace] name = "sinteractive" # Keep in sync with VERSION in `sinteractive` and the release tag (vX.Y.Z). -version = "0.5.0" +version = "0.5.1" description = "Documentation site for sinteractive" channels = ["conda-forge"] platforms = ["linux-64", "osx-arm64"] diff --git a/sinteractive b/sinteractive index 93b2257..d7b6420 100755 --- a/sinteractive +++ b/sinteractive @@ -19,7 +19,7 @@ set -u TMUX_BIN="${SINTERACTIVE_TMUX:-/usr/local/bin/tmux}" JOB_ID='' # Reported by --version; keep in sync with the release tag (vX.Y.Z). -VERSION='0.5.0' +VERSION='0.5.1' # Basename for error messages ($0 is the full install path — noisy). PROG="${0##*/}" @@ -1173,10 +1173,10 @@ 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 above the status line while it is exceeded. 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. +Storage quota is shown on a line below the status bar while it is exceeded. +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. CONTEXT exit 0 } @@ -1421,43 +1421,21 @@ function check_quota_cmd { exit 0 } -# The yellow rule between the pane and the status bar, which normally carries -# no text, doubles as a notice line. It is full width and empty, whereas -# status-right is 40 columns already shared by the Help and Detach keys — so -# an occasional hint goes here rather than crowding the primary status line. +# 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: # -# Both start_tmux and status_dot_loop set this option, so it lives in one -# function rather than being mirrored by hand the way status-left has to be. -# Width of the scrolling window, in columns. Fixed, so the rule reads the -# same at any terminal width and the notice never swallows the separator. -HINT_WIDTH=44 -function pane_border_format_string { - local hint=${1:-0} offset=${2:-0} - ((hint)) || return 0 - # Trailing separator so the message reads as a loop rather than colliding - # with its own start. ASCII only: the slice below is bash substring - # expansion, which counts bytes under a C locale, and a multibyte character - # cut in half renders as garbage. - local msg='Claude Code: run sinteractive --install-claude to enable the skills and hooks | ' - ((offset %= ${#msg})) - local window="${msg}${msg}" - printf '%s' "#[align=centre]#[fg=yellow,bold] ${window:offset:HINT_WIDTH} #[default]" -} - -# The notice line has three possible tenants, and they are ranked rather than -# queued: something you must act on outranks something you should know, which -# outranks an offer to install a feature. -# -# 1. over quota — red. Writes are failing or about to. -# 2. short session — yellow. The allocation is not the length you asked for. -# 3. Claude hint — yellow, scrolling. Purely an offer. +# 1. warnings — over quota (red) and short session (yellow). Both hold for +# the whole session, so they share their line rather than taking turns. +# 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 first two are static and short enough to read at a glance, which is the -# point: a marquee is fine for an invitation and wrong for a warning. When -# both apply they share the line, since between them they still fit a normal -# terminal and suppressing either would hide something true for the whole -# session. -function quota_border_string { +# 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. +function quota_notice_string { local over="$1" used="$2" hard="$3" over_kb="$4" ((over)) || return 1 printf '#[fg=red,bold] ⚠ OVER QUOTA %s / %s · over by %s #[default]' \ @@ -1465,26 +1443,68 @@ function quota_border_string { "$(quota_kb_to_size "$over_kb")" } -function maint_border_string { +function maint_notice_string { local name="$1" start_epoch="$2" [[ -n "$start_epoch" ]] || return 1 printf '#[fg=yellow,bold] SHORT SESSION · ends before %s at %s #[default]' \ "$name" "$(date -d "@$start_epoch" '+%a %H:%M' 2>/dev/null)" } -function compose_border { - local quota_part="$1" maint_part="$2" hint_part="$3" +# 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]' +} + +# Left-aligned, so a notice starts directly under the session glyph rather +# than floating in the middle of a line whose neighbour above it is flush +# left. Every part already carries its own padding. +function compose_notice { + local quota_part="$1" maint_part="$2" if [[ -n "$quota_part" && -n "$maint_part" ]]; then - printf '%s' "#[align=centre]${quota_part}#[fg=yellow]│#[default]${maint_part}" + printf '%s' "#[align=left]${quota_part}#[fg=yellow]│#[default]${maint_part}" elif [[ -n "$quota_part" ]]; then - printf '%s' "#[align=centre]${quota_part}" + printf '%s' "#[align=left]${quota_part}" elif [[ -n "$maint_part" ]]; then - printf '%s' "#[align=centre]${maint_part}" - else - printf '%s' "$hint_part" + printf '%s' "#[align=left]${maint_part}" 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. +# +# 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 +} + # Whether to nag about --install-claude in the status bar: only while Claude # Code is actually running in this session and the integration is not live. # @@ -2368,9 +2388,9 @@ 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 hint_offset=0 hint_tick=0 last_border='' + local hint_shown=0 hint_checked=0 last_notice='' local quota_checked=0 quota_part='' maint_part='' - maint_part=$(maint_border_string "$maint_name" "$maint_start") || 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 local now remaining # printf's builtin strftime — no date fork, which matters at the @@ -2406,21 +2426,35 @@ function status_dot_loop { quota_probe >/dev/null 2>&1 && read -r q_over q_used q_hard q_over_kb _ q_age < <(quota_cached) fi - quota_part=$(quota_border_string \ + quota_part=$(quota_notice_string \ "${q_over:-0}" "${q_used:-0}" "${q_hard:-0}" "${q_over_kb:-0}") || quota_part='' fi # Claude Code hint, on the same cadence as the scheduler poll rather than - # the loop's tick (which reaches 10Hz in the final countdown). Placed - # above the no-deadline `continue` below so the hint still works on a job - # whose end time Slurm will not report. Only re-sets the option when the - # answer changes, so a steady state costs one pgrep per poll. + # the loop's tick (which reaches 10Hz in the final countdown). Only + # re-read when the poll comes round, so a steady state costs one pgrep + # per poll. if ((now - hint_checked >= poll)); then hint_checked=$now - local want_hint=0 - claude_hint_wanted && want_hint=1 - hint_shown=$want_hint + hint_shown=0 + claude_hint_wanted && hint_shown=1 + fi + + # The lines below the status bar, 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") + 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[@]}" + last_notice="$notice" fi # One cadence for both halves of the state file: ask Slurm for the end @@ -2552,34 +2586,6 @@ function status_dot_loop { sleep_for=0.1 fi - # The notice line. A warning wins the line outright; only the Claude hint - # scrolls, and only when it has the line to itself — so a session showing - # a static warning keeps the loop's multi-second sleeps and never pays for - # the redraw, which the marquee below otherwise forces. - local hint_part='' - if ((hint_shown)) && [[ -z "$quota_part" && -z "$maint_part" ]]; then - hint_tick=$((hint_tick + 1)) - # One column per ~0.3s, held constant across phases — every tick at the - # 0.3s cadence forced below, every third tick during the red phase's - # 10Hz countdown, which keeps that countdown fluid. - if [[ "$sleep_for" == 0.1 ]]; then - ((hint_tick % 3 == 0)) && hint_offset=$((hint_offset + 1)) - else - hint_offset=$((hint_offset + 1)) - sleep_for=0.3 - fi - hint_part=$(pane_border_format_string 1 "$hint_offset") - fi - - local border - border=$(compose_border "$quota_part" "$maint_part" "$hint_part") - if [[ "$border" != "$last_border" ]]; then - # Global, like the other pane-border options in start_tmux, so panes - # and windows opened later show the same rule. - $TMUX_BIN -L "$sock" set-option -g pane-border-format "$border" 2>/dev/null - last_border="$border" - fi - local left=" ${glyph}${base}${text} " if [[ "$left" != "$last" ]]; then $TMUX_BIN -L "$sock" set-option -t "$session_name" status-left "$left" @@ -2728,10 +2734,12 @@ function start_tmux { # yellow to a darker amber keep the rule visible on white backgrounds # (hardcoded #F0E442 was ~1.3:1 contrast on white — invisible). Global so # 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. $TMUX_BIN -L "$sock" set-option -g pane-border-status bottom - # Empty by default; status_dot_loop swaps in the --install-claude hint when - # one is wanted (see pane_border_format_string). - $TMUX_BIN -L "$sock" set-option -g pane-border-format "$(pane_border_format_string 0)" + $TMUX_BIN -L "$sock" set-option -g pane-border-format '' $TMUX_BIN -L "$sock" set-option -g pane-border-lines heavy $TMUX_BIN -L "$sock" set-option -g pane-border-style 'fg=yellow' $TMUX_BIN -L "$sock" set-option -g pane-active-border-style 'fg=yellow' diff --git a/skills/bodhi-storage/SKILL.md b/skills/bodhi-storage/SKILL.md index e8250e9..cfa0a25 100644 --- a/skills/bodhi-storage/SKILL.md +++ b/skills/bodhi-storage/SKILL.md @@ -88,8 +88,8 @@ sinteractive --check-quota sinteractive --check-quota --json # same, machine-readable ``` -A session shows a red `OVER QUOTA` notice above its status line while this -holds, refreshed every ten minutes. +A session shows a red `OVER QUOTA` notice on a line below its status bar +while this holds, 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