From e11f6260a019a980922da9ecbcbe57442f41af18 Mon Sep 17 00:00:00 2001 From: Jay Hesselberth Date: Fri, 18 Sep 2026 06:26:46 -0600 Subject: [PATCH 1/2] feat(list): read CWD live from /proc and reorder table columns list's CWD column always showed "-" since zellij has no tmux-style pane-cwd query; it now reads the live cwd from /proc via the job's cgroup over ssh. The default human table is job id, name, remaining, cwd; --full adds node, partition, elapsed/limit. --- CHANGELOG.md | 14 +++ completions/sinteractive.bash | 40 +++++- completions/sinteractive.fish | 64 +++++----- completions/sinteractive.zsh | 24 ++++ crates/sint-core/src/metrics/mod.rs | 2 + crates/sint-core/src/metrics/pane.rs | 159 ++++++++++++++++++++++++ crates/sint/src/cli.rs | 21 +++- crates/sint/src/commands/common.rs | 34 ++++-- crates/sint/src/commands/list.rs | 161 +++++++++++++++++++------ crates/sint/src/commands/mod.rs | 1 + crates/sint/src/commands/statusline.rs | 10 +- crates/sint/tests/reporting.rs | 37 ++++-- docs/usage.md | 7 +- 13 files changed, 471 insertions(+), 103 deletions(-) create mode 100644 crates/sint-core/src/metrics/pane.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6c154..ad377c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ and this project adheres to ## [Unreleased] +### Added + +- `sinteractive list --full`, which adds node, partition and elapsed/limit + to the human table. The default now shows job id, name, time remaining + and cwd instead of node/partition/elapsed/timelimit/cwd, so the common + case fits without the columns most people never look up. + +### Fixed + +- `list`'s CWD column, which always showed `-`. It now reads the pane's + live working directory from `/proc//cwd` on the session's node + (zellij, unlike tmux, has no built-in query for a pane's current + directory), fetched over ssh in parallel across sessions. + ## [1.3.0] - 2026-09-07 ### Added diff --git a/completions/sinteractive.bash b/completions/sinteractive.bash index cb57af9..cc8b507 100644 --- a/completions/sinteractive.bash +++ b/completions/sinteractive.bash @@ -22,6 +22,9 @@ _sinteractive() { sinteractive,__job) cmd="sinteractive__subcmd____job" ;; + sinteractive,__pane-cwd) + cmd="sinteractive__subcmd____pane__subcmd__cwd" + ;; sinteractive,__popup) cmd="sinteractive__subcmd____popup" ;; @@ -223,6 +226,9 @@ _sinteractive() { sinteractive__subcmd__help,__job) cmd="sinteractive__subcmd__help__subcmd____job" ;; + sinteractive__subcmd__help,__pane-cwd) + cmd="sinteractive__subcmd__help__subcmd____pane__subcmd__cwd" + ;; sinteractive__subcmd__help,__popup) cmd="sinteractive__subcmd__help__subcmd____popup" ;; @@ -446,7 +452,7 @@ _sinteractive() { case "${cmd}" in sinteractive) - opts="-p -t -j -m -n -l -a -h -V --node --partition --time --threads --mem --name --mouse --no-mouse --detach --json --status --refresh --list --ensure --attach --cancel --check-quota --agent-context --install-claude --help --version launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" + opts="-p -t -j -m -n -l -a -h -V --node --partition --time --threads --mem --name --mouse --no-mouse --detach --json --status --refresh --list --ensure --attach --cancel --check-quota --agent-context --install-claude --help --version launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -563,6 +569,20 @@ _sinteractive() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + sinteractive__subcmd____pane__subcmd__cwd) + opts="-h --help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; sinteractive__subcmd____popup) opts="-h --help monitor queue help notices rename" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -1270,7 +1290,7 @@ _sinteractive() { return 0 ;; sinteractive__subcmd__help) - opts="launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" + opts="launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1311,6 +1331,20 @@ _sinteractive() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + sinteractive__subcmd__help__subcmd____pane__subcmd__cwd) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; sinteractive__subcmd__help__subcmd____popup) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -2266,7 +2300,7 @@ _sinteractive() { return 0 ;; sinteractive__subcmd__list) - opts="-h --json --help" + opts="-h --full --json --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/completions/sinteractive.fish b/completions/sinteractive.fish index d6974ae..908d350 100644 --- a/completions/sinteractive.fish +++ b/completions/sinteractive.fish @@ -74,6 +74,7 @@ complete -c sinteractive -n "__fish_sinteractive_needs_command" -f -a "schema" - complete -c sinteractive -n "__fish_sinteractive_needs_command" -f -a "__job" -d 'The batch job body: starts zellij on the node and babysits it' complete -c sinteractive -n "__fish_sinteractive_needs_command" -f -a "__attach" -d 'Runs on the node over ssh: attach the local zellij client' complete -c sinteractive -n "__fish_sinteractive_needs_command" -f -a "__popup" -d 'In-session floating views' +complete -c sinteractive -n "__fish_sinteractive_needs_command" -f -a "__pane-cwd" -d 'Runs on the node over ssh: the pane\'s live working directory' complete -c sinteractive -n "__fish_sinteractive_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c sinteractive -n "__fish_sinteractive_using_subcommand launch" -l node -d 'Request a specific compute node (`--nodelist`)' -r complete -c sinteractive -n "__fish_sinteractive_using_subcommand launch" -s p -l partition -d 'Slurm partition' -r @@ -88,6 +89,7 @@ complete -c sinteractive -n "__fish_sinteractive_using_subcommand launch" -l jso complete -c sinteractive -n "__fish_sinteractive_using_subcommand launch" -s h -l help -d 'Print help' complete -c sinteractive -n "__fish_sinteractive_using_subcommand attach" -l ssh -d 'Attach over ssh -X (X11 forwarding) instead of srun --overlap' complete -c sinteractive -n "__fish_sinteractive_using_subcommand attach" -s h -l help -d 'Print help' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand list" -l full -d 'Also show node, partition, and elapsed/limit' complete -c sinteractive -n "__fish_sinteractive_using_subcommand list" -l json -d 'Machine-readable JSON output' complete -c sinteractive -n "__fish_sinteractive_using_subcommand list" -s h -l help -d 'Print help' complete -c sinteractive -n "__fish_sinteractive_using_subcommand status" -l refresh -d 'Re-check the time budget against Slurm now and update the cache' @@ -228,36 +230,38 @@ complete -c sinteractive -n "__fish_sinteractive_using_subcommand __job" -l mous complete -c sinteractive -n "__fish_sinteractive_using_subcommand __job" -s h -l help -d 'Print help' complete -c sinteractive -n "__fish_sinteractive_using_subcommand __attach" -s h -l help -d 'Print help' complete -c sinteractive -n "__fish_sinteractive_using_subcommand __popup" -s h -l help -d 'Print help' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "launch" -d 'Launch a new session (the default when no subcommand is given)' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "attach" -d 'Reattach to a session by JOBID or NAME (your only session when omitted)' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "list" -d 'List running sessions' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "status" -d 'Show one session\'s status' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "cancel" -d 'Cancel a session' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "queue" -d 'Your job queue: running, pending (with reasons), and recent history' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "monitor" -d 'Live CPU/GPU/process view of a session\'s node, or any host' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "quota" -d 'Storage quota (Bodhi quota daemons; unavailable on other clusters)' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "doctor" -d 'Check this install and, optionally, every compute node' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "session" -d 'Drive a session from outside: ensure, peek, send, events' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "claude" -d 'Claude Code integration: install, context, hook, statusline, mcp' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "gen" -d 'Generated output: completions, man page, JSON schemas' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "ensure" -d 'Superseded by `session ensure`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "peek" -d 'Superseded by `session peek`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "send" -d 'Superseded by `session send`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "events" -d 'Superseded by `session events`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "refresh" -d 'Superseded by `status --refresh`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "snapshot" -d 'Superseded by `monitor --once`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "agent-context" -d 'Superseded by `claude context`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "hook" -d 'Superseded by `claude hook`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "statusline" -d 'Superseded by `claude statusline`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "mcp" -d 'Superseded by `claude mcp`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "install-claude" -d 'Superseded by `claude install`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "completions" -d 'Superseded by `gen completions`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "man" -d 'Superseded by `gen man`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "schema" -d 'Superseded by `gen schema`' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "__job" -d 'The batch job body: starts zellij on the node and babysits it' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "__attach" -d 'Runs on the node over ssh: attach the local zellij client' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "__popup" -d 'In-session floating views' -complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand __pane-cwd" -s h -l help -d 'Print help' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "launch" -d 'Launch a new session (the default when no subcommand is given)' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "attach" -d 'Reattach to a session by JOBID or NAME (your only session when omitted)' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "list" -d 'List running sessions' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "status" -d 'Show one session\'s status' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "cancel" -d 'Cancel a session' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "queue" -d 'Your job queue: running, pending (with reasons), and recent history' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "monitor" -d 'Live CPU/GPU/process view of a session\'s node, or any host' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "quota" -d 'Storage quota (Bodhi quota daemons; unavailable on other clusters)' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "doctor" -d 'Check this install and, optionally, every compute node' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "session" -d 'Drive a session from outside: ensure, peek, send, events' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "claude" -d 'Claude Code integration: install, context, hook, statusline, mcp' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "gen" -d 'Generated output: completions, man page, JSON schemas' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "ensure" -d 'Superseded by `session ensure`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "peek" -d 'Superseded by `session peek`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "send" -d 'Superseded by `session send`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "events" -d 'Superseded by `session events`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "refresh" -d 'Superseded by `status --refresh`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "snapshot" -d 'Superseded by `monitor --once`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "agent-context" -d 'Superseded by `claude context`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "hook" -d 'Superseded by `claude hook`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "statusline" -d 'Superseded by `claude statusline`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "mcp" -d 'Superseded by `claude mcp`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "install-claude" -d 'Superseded by `claude install`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "completions" -d 'Superseded by `gen completions`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "man" -d 'Superseded by `gen man`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "schema" -d 'Superseded by `gen schema`' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "__job" -d 'The batch job body: starts zellij on the node and babysits it' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "__attach" -d 'Runs on the node over ssh: attach the local zellij client' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "__popup" -d 'In-session floating views' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "__pane-cwd" -d 'Runs on the node over ssh: the pane\'s live working directory' +complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and not __fish_seen_subcommand_from launch attach list status cancel queue monitor quota doctor session claude gen ensure peek send events refresh snapshot agent-context hook statusline mcp install-claude completions man schema __job __attach __popup __pane-cwd help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and __fish_seen_subcommand_from session" -f -a "ensure" -d 'Reuse the session named NAME, or launch it if absent (implies --detach)' complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and __fish_seen_subcommand_from session" -f -a "peek" -d 'Read the last lines of a session\'s screen' complete -c sinteractive -n "__fish_sinteractive_using_subcommand help; and __fish_seen_subcommand_from session" -f -a "send" -d 'Type a command into a session\'s shell' diff --git a/completions/sinteractive.zsh b/completions/sinteractive.zsh index 0aa9cf0..306704a 100644 --- a/completions/sinteractive.zsh +++ b/completions/sinteractive.zsh @@ -85,6 +85,7 @@ _arguments "${_arguments_options[@]}" : \ ;; (list) _arguments "${_arguments_options[@]}" : \ +'--full[Also show node, partition, and elapsed/limit]' \ '--json[Machine-readable JSON output]' \ '-h[Print help]' \ '--help[Print help]' \ @@ -736,6 +737,13 @@ _arguments "${_arguments_options[@]}" : \ '::job_id -- Defaults to `SINTERACTIVE_JOB_ID` (set in every session pane):_default' \ && ret=0 ;; +(__pane-cwd) +_arguments "${_arguments_options[@]}" : \ +'-h[Print help]' \ +'--help[Print help]' \ +':job_id:_default' \ +&& ret=0 +;; (help) _arguments "${_arguments_options[@]}" : \ ":: :_sinteractive__subcmd__help_commands" \ @@ -1012,6 +1020,10 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ && ret=0 ;; +(__pane-cwd) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; (help) _arguments "${_arguments_options[@]}" : \ && ret=0 @@ -1057,6 +1069,7 @@ _sinteractive_commands() { '__job:The batch job body\: starts zellij on the node and babysits it' \ '__attach:Runs on the node over ssh\: attach the local zellij client' \ '__popup:In-session floating views' \ +'__pane-cwd:Runs on the node over ssh\: the pane'\''s live working directory' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'sinteractive commands' commands "$@" @@ -1071,6 +1084,11 @@ _sinteractive__subcmd____job_commands() { local commands; commands=() _describe -t commands 'sinteractive __job commands' commands "$@" } +(( $+functions[_sinteractive__subcmd____pane-cwd_commands] )) || +_sinteractive__subcmd____pane-cwd_commands() { + local commands; commands=() + _describe -t commands 'sinteractive __pane-cwd commands' commands "$@" +} (( $+functions[_sinteractive__subcmd____popup_commands] )) || _sinteractive__subcmd____popup_commands() { local commands; commands=() @@ -1382,6 +1400,7 @@ _sinteractive__subcmd__help_commands() { '__job:The batch job body\: starts zellij on the node and babysits it' \ '__attach:Runs on the node over ssh\: attach the local zellij client' \ '__popup:In-session floating views' \ +'__pane-cwd:Runs on the node over ssh\: the pane'\''s live working directory' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'sinteractive help commands' commands "$@" @@ -1396,6 +1415,11 @@ _sinteractive__subcmd__help__subcmd____job_commands() { local commands; commands=() _describe -t commands 'sinteractive help __job commands' commands "$@" } +(( $+functions[_sinteractive__subcmd__help__subcmd____pane-cwd_commands] )) || +_sinteractive__subcmd__help__subcmd____pane-cwd_commands() { + local commands; commands=() + _describe -t commands 'sinteractive help __pane-cwd commands' commands "$@" +} (( $+functions[_sinteractive__subcmd__help__subcmd____popup_commands] )) || _sinteractive__subcmd__help__subcmd____popup_commands() { local commands; commands=() diff --git a/crates/sint-core/src/metrics/mod.rs b/crates/sint-core/src/metrics/mod.rs index 4bfec18..6dc36f4 100644 --- a/crates/sint-core/src/metrics/mod.rs +++ b/crates/sint-core/src/metrics/mod.rs @@ -5,6 +5,7 @@ //! - [`cpu`] — `/proc/stat`, `/proc/loadavg`, `/proc/meminfo` parsers //! - [`procs`] — per-process rows with two-sample CPU% //! - [`gpu`] — NVML, loaded lazily; empty without a driver +//! - [`pane`] — the session pane's live working directory, for `list` //! //! A [`Sampler`] holds the between-sample state (previous counters, the //! NVML handle, the CPU history ring). Call [`Sampler::sample`] at ≥ 1 s @@ -17,6 +18,7 @@ pub mod cgroup; pub mod cpu; pub mod gpu; +pub mod pane; pub mod procs; use std::collections::{BTreeSet, HashMap, VecDeque}; diff --git a/crates/sint-core/src/metrics/pane.rs b/crates/sint-core/src/metrics/pane.rs new file mode 100644 index 0000000..8ed99bf --- /dev/null +++ b/crates/sint-core/src/metrics/pane.rs @@ -0,0 +1,159 @@ +//! The session pane's live working directory — `list`'s CWD column. +//! +//! zellij has no client-facing "what directory is this pane in" query: the +//! tmux equivalent 0.x used, `display-message -p '#{pane_current_path}'`, +//! has no zellij counterpart (`dump-layout` only ever reports the directory +//! a pane was *opened* with, not where a later `cd` left it). The only place +//! the live answer exists is `/proc//cwd` of the pane's own shell +//! process, so this reads it directly from the job's cgroup. + +use std::collections::BTreeSet; +use std::path::Path; + +use super::cgroup::{JobCgroup, CGROUP_ROOT}; + +/// The shell every sinteractive pane starts +/// (`assets/zellij/config.kdl`'s `default_shell`). Kept in sync by hand; a +/// mismatch just means the CWD column reports nothing, same as before this +/// existed. +pub const PANE_SHELL: &str = "bash"; + +/// One candidate process for [`pick_pane_shell`]: enough of +/// `/proc//stat` to tell the pane's own shell from a subshell it later +/// ran. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ShellCandidate { + pub pid: u32, + pub comm: String, + pub starttime: u64, +} + +/// The pane's own shell among a set of candidate processes: the oldest one +/// named [`PANE_SHELL`]. The pane's shell starts once, at session creation; +/// anything a user later runs inside it that shares the name — a subshell, +/// `bash script.sh`, a Makefile recipe — starts later and so has a later +/// `starttime`. +pub fn pick_pane_shell(candidates: impl IntoIterator) -> Option { + candidates + .into_iter() + .filter(|c| c.comm == PANE_SHELL) + .min_by_key(|c| c.starttime) + .map(|c| c.pid) +} + +fn stat_candidates(pids: &BTreeSet) -> Vec { + pids.iter() + .filter_map(|&pid| { + let stat = procfs::process::Process::new(pid as i32) + .ok()? + .stat() + .ok()?; + Some(ShellCandidate { + pid, + comm: stat.comm, + starttime: stat.starttime, + }) + }) + .collect() +} + +/// [`pick_pane_shell`]'s pid, read live from `/proc` for every pid in +/// `pids` (normally a [`JobCgroup::pids`]). +pub fn pane_shell_pid(pids: &BTreeSet) -> Option { + pick_pane_shell(stat_candidates(pids)) +} + +/// A job's pane's live working directory, with the cgroup mount point made +/// explicit (tests; see [`super::Sampler::with_cgroup_root`] for the same +/// pattern). `None` when the job's cgroup or its shell cannot be found — +/// a finished session, no permission, an unsupported cgroup layout. +pub fn session_cwd_in(root: &Path, job_id: u64, uid: u32) -> Option { + let cgroup = JobCgroup::find(root, job_id, uid)?; + let pid = pane_shell_pid(&cgroup.pids())?; + let cwd = procfs::process::Process::new(pid as i32).ok()?.cwd().ok()?; + cwd.to_str().map(str::to_string) +} + +/// [`session_cwd_in`] against the real cgroup mount, for this user. +pub fn session_cwd(job_id: u64) -> Option { + // SAFETY: getuid has no preconditions. + let uid = unsafe { libc::getuid() }; + session_cwd_in(Path::new(CGROUP_ROOT), job_id, uid) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::io::Write; + use std::process::{Command, Stdio}; + + fn cand(pid: u32, comm: &str, starttime: u64) -> ShellCandidate { + ShellCandidate { + pid, + comm: comm.to_string(), + starttime, + } + } + + #[test] + fn picks_the_oldest_bash() { + let candidates = [ + cand(200, "bash", 500), + cand(100, "bash", 300), + cand(300, "vim", 100), + ]; + assert_eq!(pick_pane_shell(candidates), Some(100)); + } + + #[test] + fn ignores_non_shell_processes() { + let candidates = [cand(1, "sleep", 10), cand(2, "python3", 20)]; + assert_eq!(pick_pane_shell(candidates), None); + } + + #[test] + fn empty_is_none() { + assert_eq!(pick_pane_shell(Vec::::new()), None); + } + + /// A real `bash` child, kept alive with piped stdin so nothing execs + /// over it, is found through a fake cgroup tree and its real cwd read + /// back — the whole [`session_cwd_in`] path short of a live cgroup. + #[test] + fn finds_a_running_pane_through_a_fake_cgroup() { + let work = tempfile::tempdir().unwrap(); + let mut child = Command::new("bash") + .current_dir(work.path()) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("spawn bash"); + let pid = child.id(); + + let cgroup_root = tempfile::tempdir().unwrap(); + // v2 (unified) layout: `cgroup.controllers` at the root is what + // `JobCgroup::find` keys off to pick this branch over v1's + // per-controller directories. + fs::write(cgroup_root.path().join("cgroup.controllers"), "").unwrap(); + let job_dir = cgroup_root.path().join("slurm/uid_0/job_555"); + fs::create_dir_all(&job_dir).unwrap(); + fs::write(job_dir.join("cgroup.procs"), format!("{pid}\n")).unwrap(); + + let cwd = session_cwd_in(cgroup_root.path(), 555, 0); + + // Tear down before asserting, so a failed assertion never leaks it. + let _ = child.stdin.take().map(|mut s| s.write_all(b"\n")); + let _ = child.kill(); + let _ = child.wait(); + + assert_eq!(cwd.as_deref(), work.path().canonicalize().unwrap().to_str()); + } + + #[test] + fn no_cgroup_is_none() { + let root = tempfile::tempdir().unwrap(); + assert_eq!(session_cwd_in(root.path(), 999, 0), None); + } +} diff --git a/crates/sint/src/cli.rs b/crates/sint/src/cli.rs index 1798619..308fc5b 100644 --- a/crates/sint/src/cli.rs +++ b/crates/sint/src/cli.rs @@ -140,7 +140,7 @@ pub enum Command { /// Reattach to a session by JOBID or NAME (your only session when omitted) Attach(AttachArgs), /// List running sessions - List(JsonFlag), + List(ListArgs), /// Show one session's status Status(TargetArgs), /// Cancel a session @@ -225,6 +225,9 @@ pub enum Command { /// Defaults to `SINTERACTIVE_JOB_ID` (set in every session pane) job_id: Option, }, + /// Runs on the node over ssh: the pane's live working directory + #[command(name = "__pane-cwd", hide = true)] + PaneCwd { job_id: u64 }, } /// `sinteractive session …` — the verbs that act on a session you are not @@ -296,6 +299,16 @@ pub struct JsonFlag { pub json: bool, } +#[derive(Args, Debug, Clone, Default)] +pub struct ListArgs { + /// Also show node, partition, and elapsed/limit + #[arg(long)] + pub full: bool, + /// Machine-readable JSON output + #[arg(long)] + pub json: bool, +} + #[derive(Args, Debug, Clone, Default)] pub struct TargetArgs { /// JOBID or NAME; defaults to the current session @@ -465,7 +478,7 @@ impl Cli { return (cmd, false); } let cmd = if c.compat_list { - Command::List(JsonFlag { json }) + Command::List(ListArgs { full: false, json }) } else if let Some(t) = &c.compat_status { Command::Status(TargetArgs { target: target(&Some(t.clone())), @@ -926,11 +939,11 @@ mod tests { #[test] fn compat_list_and_flags() { let (cmd, _, dep) = parse(&["-l", "--json"]); - assert!(matches!(cmd, Command::List(JsonFlag { json: true }))); + assert!(matches!(cmd, Command::List(ListArgs { json: true, .. }))); assert!(dep); assert!(matches!( parse(&["--list"]).0, - Command::List(JsonFlag { json: false }) + Command::List(ListArgs { json: false, .. }) )); assert!(matches!( parse(&["--cancel", "web"]).0, diff --git a/crates/sint/src/commands/common.rs b/crates/sint/src/commands/common.rs index 6ec6120..082d9c8 100644 --- a/crates/sint/src/commands/common.rs +++ b/crates/sint/src/commands/common.rs @@ -280,15 +280,7 @@ pub fn render_status(info: &SessionInfo, p: &Palette) -> String { info.time_limit.as_deref().unwrap_or("") )); if let Some(remaining) = info.remaining_seconds { - // The one number worth reading at a glance, so it is coloured by - // how much of it is left rather than left the same shade all session. - let rem_c = if remaining < 900 { - &p.err - } else if remaining < 3600 { - &p.warn - } else { - &p.ok - }; + let rem_c = remaining_colour(remaining, p); out.push_str(&format!( "{}{rem_c}{}{reset}\n", field("Remaining:"), @@ -298,6 +290,30 @@ pub fn render_status(info: &SessionInfo, p: &Palette) -> String { out } +/// Colour for a remaining-time budget: red under 15 minutes, yellow under an +/// hour, green otherwise — the one number worth reading at a glance, so it +/// is coloured by how much of it is left rather than left the same shade all +/// session. Shared by `status`'s `Remaining:` line and `list`'s REMAINING +/// column. +pub fn remaining_colour(remaining: i64, p: &Palette) -> &str { + if remaining < 900 { + &p.err + } else if remaining < 3600 { + &p.warn + } else { + &p.ok + } +} + +/// Collapse a `$HOME` prefix to `~`, the way every path this tool shows a +/// person does. +pub fn tilde(path: &str) -> String { + match std::env::var("HOME") { + Ok(h) if !h.is_empty() && path.starts_with(&h) => format!("~{}", &path[h.len()..]), + _ => path.to_string(), + } +} + /// [`render_status`] on stdout, followed by the session's active notices — /// the full text behind the status line's "⚠ N notices" indicator, readable /// without attaching (script line 1127). What `status`, `refresh` and diff --git a/crates/sint/src/commands/list.rs b/crates/sint/src/commands/list.rs index a0db9e2..8e9e6c3 100644 --- a/crates/sint/src/commands/list.rs +++ b/crates/sint/src/commands/list.rs @@ -1,18 +1,27 @@ -//! `sinteractive list [--json]` — the user's running sessions. Ports -//! `list_sessions` (script lines 921-1010). +//! `sinteractive list [--full] [--json]` — the user's running sessions. +//! Ports `list_sessions` (script lines 921-1010). //! //! Only RUNNING sessions are listed, as in 0.x. The JSON rows share the -//! `status --json` shape and additionally carry `cwd`. +//! `status --json` shape and additionally carry `cwd`. `--full` adds node, +//! partition and elapsed/limit to the human table; the default keeps to +//! what fits at a glance: job id, name, time remaining, cwd. + +use std::thread; use anyhow::Result; use serde::Serialize; +use sint_core::color::Palette; +use sint_core::metrics::pane; use sint_core::session::SessionInfo; +use sint_core::slurm::squeue::JobRow; +use sint_core::time::format_short_duration; -use super::common::{print_json, Ctx}; -use crate::cli::JsonFlag; +use super::common::{current_exe, print_json, remaining_colour, ssh_batch, tilde, Ctx}; +use crate::cli::ListArgs; +use crate::zellij_cmd::shell_quote; -/// One `list --json` row: the status object plus `cwd`, which is always -/// present (null until phase 2 asks the node) and always the last key. +/// One `list --json` row: the status object plus `cwd`, always present +/// (null when it could not be found) and always the last key. #[derive(Debug, Clone, Serialize, schemars::JsonSchema)] pub struct ListRow { #[serde(flatten)] @@ -20,23 +29,81 @@ pub struct ListRow { pub cwd: Option, } +/// `cwd` for every row, fetched over ssh in parallel — one `sinteractive +/// __pane-cwd JOBID` per node, backgrounded and joined the way 0.x +/// backgrounded one `tmux display-message` per session and `wait`ed (script +/// line 905). `None` on any failure — unreachable node, no cgroup yet, no +/// matching shell — so `list` never blocks or errors on a session whose cwd +/// can't be found. +fn fetch_cwds(rows: &[JobRow]) -> Vec> { + let Ok(exe) = current_exe() else { + return vec![None; rows.len()]; + }; + let exe = shell_quote(&exe.to_string_lossy()); + let handles: Vec<_> = rows + .iter() + .map(|row| { + let node = row.node.clone(); + let job_id = row.job_id; + let exe = exe.clone(); + thread::spawn(move || { + let remote = format!("{exe} __pane-cwd {job_id}"); + let out = ssh_batch(&node, 3, &remote).output().ok()?; + let cwd = String::from_utf8_lossy(&out.stdout).trim().to_string(); + (!cwd.is_empty()).then_some(cwd) + }) + }) + .collect(); + handles + .into_iter() + .map(|h| h.join().unwrap_or(None)) + .collect() +} + +/// `sinteractive __pane-cwd JOBID` — runs on the node over ssh: the pane's +/// live working directory, `~`-collapsed, printed bare with a trailing +/// newline. Prints nothing when it cannot be found (a finished session, no +/// cgroup yet, no matching shell); [`fetch_cwds`] reads an empty line as +/// "unknown", same as an ssh failure. +pub fn run_pane_cwd(job_id: u64) -> Result { + if let Some(cwd) = pane::session_cwd(job_id) { + println!("{}", tilde(&cwd)); + } + Ok(0) +} + /// The `list --json` rows: the user's RUNNING sessions, in squeue order. pub fn list_data(ctx: &Ctx) -> Result> { let now = sint_core::now_epoch(); - Ok(ctx - .running_sessions()? + let rows = ctx.running_sessions()?; + let cwds = fetch_cwds(&rows); + Ok(rows .iter() - .map(|row| ListRow { + .zip(cwds) + .map(|(row, cwd)| ListRow { info: SessionInfo::from_row(row, now), - // TODO(phase-2): cwd via zellij list-panes on the node (0.x asked - // tmux over ssh, script line 905). Until then every row reports - // null; the key is part of the contract so it is always present. - cwd: None, + cwd, }) .collect()) } -pub fn run(args: JsonFlag) -> Result { +/// The padded, coloured REMAINING cell: [`remaining_colour`] applied +/// outside the padding, so the escape it wraps never counts toward the +/// column width. +fn remaining_cell(remaining: Option, width: usize, p: &Palette) -> String { + match remaining { + Some(r) => format!( + "{}{: format!("{}{: Result { let ctx = Ctx::new(); if args.json { print_json(&list_data(&ctx)?)?; @@ -50,38 +117,52 @@ pub fn run(args: JsonFlag) -> Result { println!("Start one with {}sinteractive{}.", p.key, p.reset); return Ok(0); } - let cwd: Option = None; + let cwds = fetch_cwds(&rows); // Colour goes outside every padded field, never inside it: an escape // counted as width would shift every column to its right. let p = ctx.palette(1); - println!( - "{}{:<10} {:<20} {:<14} {:<12} {:<10} {:<10} CWD{}", - p.dim, "JOBID", "NAME", "NODE", "PARTITION", "ELAPSED", "TIMELIMIT", p.reset - ); - for row in &rows { - let info = SessionInfo::from_row(row, 0); - let name = info.name.as_deref().unwrap_or("-"); - let cwd = cwd.as_deref().unwrap_or("-"); + if args.full { println!( - "{}{:<10}{} {}{:<20}{} {}{:<14}{} {:<12} {:<10} {:<10} {}{}{}", - p.id, - row.job_id, - p.reset, - p.bold, - name, - p.reset, - p.id, - row.node, - p.reset, - row.partition, - row.elapsed, - row.time_limit, - p.dim, - cwd, - p.reset + "{}{:<10} {:<20} {:<14} {:<12} {:<20} {:<10} CWD{}", + p.dim, "JOBID", "NAME", "NODE", "PARTITION", "ELAPSED/LIMIT", "REMAINING", p.reset + ); + } else { + println!( + "{}{:<10} {:<20} {:<10} CWD{}", + p.dim, "JOBID", "NAME", "REMAINING", p.reset ); } + for (row, cwd) in rows.iter().zip(&cwds) { + let info = SessionInfo::from_row(row, sint_core::now_epoch()); + let name = info.name.as_deref().unwrap_or("-"); + let cwd = cwd.as_deref().unwrap_or("-"); + let remaining = remaining_cell(info.remaining_seconds, 10, &p); + if args.full { + println!( + "{}{:<10}{} {}{:<20}{} {}{:<14}{} {:<12} {:<20} {remaining} {}{}{}", + p.id, + row.job_id, + p.reset, + p.bold, + name, + p.reset, + p.id, + row.node, + p.reset, + row.partition, + format!("{}/{}", row.elapsed, row.time_limit), + p.dim, + cwd, + p.reset + ); + } else { + println!( + "{}{:<10}{} {}{:<20}{} {remaining} {}{}{}", + p.id, row.job_id, p.reset, p.bold, name, p.reset, p.dim, cwd, p.reset + ); + } + } println!(); println!( diff --git a/crates/sint/src/commands/mod.rs b/crates/sint/src/commands/mod.rs index a8c9a4b..a3cd052 100644 --- a/crates/sint/src/commands/mod.rs +++ b/crates/sint/src/commands/mod.rs @@ -74,6 +74,7 @@ pub fn dispatch(command: Command) -> Result { Command::Job(args) => job::run(args), Command::AttachLocal { session } => attach_local::run(&session), Command::Popup { view, job_id } => popup::run(view, job_id), + Command::PaneCwd { job_id } => list::run_pane_cwd(job_id), } } diff --git a/crates/sint/src/commands/statusline.rs b/crates/sint/src/commands/statusline.rs index 4d95144..ebb909f 100644 --- a/crates/sint/src/commands/statusline.rs +++ b/crates/sint/src/commands/statusline.rs @@ -22,6 +22,8 @@ use serde_json::Value; use sint_core::color::Palette; use sint_core::config::ColorMode; +use super::common::tilde; + /// How wide the working directory may be before it is shortened. const CWD_WIDTH: usize = 36; @@ -59,14 +61,6 @@ pub fn parse_claude_status(json: &str) -> ClaudeStatus { } } -/// Abbreviate `$HOME` to `~`. -fn tilde(path: &str) -> String { - match std::env::var("HOME") { - Ok(h) if !h.is_empty() && path.starts_with(&h) => format!("~{}", &path[h.len()..]), - _ => path.to_string(), - } -} - /// The working directory, narrow enough that a deep tree cannot push the /// rest of the line off the terminal. /// diff --git a/crates/sint/tests/reporting.rs b/crates/sint/tests/reporting.rs index 4089ea6..5d034c4 100644 --- a/crates/sint/tests/reporting.rs +++ b/crates/sint/tests/reporting.rs @@ -312,19 +312,42 @@ fn list_human_table() { Job::new(147846, "sinteractive").node("node03"), Job::new(147900, "cargo-ci"), ]); + // The fixture's end time is in the past, so remaining clamps at zero + // and renders as `0s` (format_short_duration's under-a-minute case). fx.sinteractive().arg("list").assert().success().stdout( + predicate::str::contains("JOBID NAME REMAINING CWD\n") + .and(predicate::str::contains( + "147845 web 0s -\n", + )) + .and(predicate::str::contains( + "147846 - 0s -\n", + )) + .and(predicate::str::contains( + "Reattach: sinteractive attach JOBID|NAME\n", + )) + .and(predicate::str::contains( + "Cancel: sinteractive cancel JOBID|NAME\n", + )) + .and(predicate::str::contains("147900").not()), + ); +} + +#[test] +fn list_human_table_full_adds_node_partition_and_elapsed_limit() { + let fx = FakeSlurm::with_jobs(&[ + Job::new(147845, "sinteractive:web"), + Job::new(147846, "sinteractive").node("node03"), + ]); + fx.sinteractive().args(["list", "--full"]).assert().success().stdout( predicate::str::contains( - "JOBID NAME NODE PARTITION ELAPSED TIMELIMIT CWD\n", + "JOBID NAME NODE PARTITION ELAPSED/LIMIT REMAINING CWD\n", ) .and(predicate::str::contains( - "147845 web node01 interactive 1:02:03 8:00:00 -\n", + "147845 web node01 interactive 1:02:03/8:00:00 0s -\n", )) .and(predicate::str::contains( - "147846 - node03 interactive 1:02:03 8:00:00 -\n", - )) - .and(predicate::str::contains("Reattach: sinteractive attach JOBID|NAME\n")) - .and(predicate::str::contains("Cancel: sinteractive cancel JOBID|NAME\n")) - .and(predicate::str::contains("147900").not()), + "147846 - node03 interactive 1:02:03/8:00:00 0s -\n", + )), ); } diff --git a/docs/usage.md b/docs/usage.md index 4d6d1f0..b4d1c4a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -244,8 +244,11 @@ node: ```bash # List your running sessions sinteractive list -# JOBID NAME NODE PARTITION ELAPSED TIMELIMIT CWD -# 12345 rna-seq compute01 cpu 01:23:45 1-00:00:00 ~/projects/rna-seq +# JOBID NAME REMAINING CWD +# 12345 rna-seq 22h 36m ~/projects/rna-seq + +# Node, partition, and elapsed/limit as well +sinteractive list --full # Reattach sinteractive attach 12345 From eef240506182b1632fab22d1716e24491cc113d7 Mon Sep 17 00:00:00 2001 From: Jay Hesselberth Date: Fri, 18 Sep 2026 10:52:47 -0600 Subject: [PATCH 2/2] fix(cli): stop hardcoding the version string in --version test The literal went stale on every release bump; derive it from CARGO_PKG_VERSION like the binary itself does. --- crates/sint/tests/cli.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/sint/tests/cli.rs b/crates/sint/tests/cli.rs index 1567091..71d985b 100644 --- a/crates/sint/tests/cli.rs +++ b/crates/sint/tests/cli.rs @@ -95,17 +95,18 @@ fn the_pre_grouping_names_still_resolve() { #[test] fn version_prints_the_workspace_version() { + let expected = format!("sinteractive {}\n", env!("CARGO_PKG_VERSION")); let fx = FakeSlurm::new(); fx.sinteractive() .arg("--version") .assert() .success() - .stdout("sinteractive 1.2.0\n"); + .stdout(expected.clone()); fx.sinteractive() .arg("-V") .assert() .success() - .stdout("sinteractive 1.2.0\n"); + .stdout(expected); } #[test]