`F5` and `Ctrl-R` drop the process cache, `SHIFT+LEFT/RIGHT` step the sort
column through v4's own loop, `LEFT`/`RIGHT` scroll the command column, and
`--arrow-keys-sort` swaps the two pairs (v4 issue #3385).
THE SWAPPABLE PAIR IS RESOLVED, NOT TABULATED. The same keycode does different
things in the two configurations, so a static entry would describe the wrong
binding in one of them -- and "the `h` overlay cannot drift from what the TUI
does" is the property 2.X-a built and every chantier since has kept. One
method returns the four bindings; the dispatcher and the overlay both call it,
so the help is right by construction rather than by care. Checked both ways
round.
THE SCROLL MOVES THE ARGUMENTS ONLY. The executable name -- and its path
prefix in full mode -- stays put: it is the part that identifies the row, and
scrolling the whole cell would push it off the left edge. v4 does the same
(processlist/__init__.py:566-567), and `programlist` shares the renderer so
both blocks behave alike. A `…` marks a scrolled row even when its arguments
have run out, because losing the column silently would leave no clue that the
text is off to the left.
The sort position is READ BACK from the engine rather than tracked here, so a
sort set by `c`/`m`/`u` and one set by the arrows are the same thing. A key
the loop does not carry (`set_sort_key('auto')` leaves one) starts from the
front instead of raising.
A GAP 2.X-b1 LEFT, found in a pty rather than in a test: `SHIFT+arrow` worked
immediately and plain `LEFT`/`RIGHT` did nothing at all. The escape resolver
knew `[A`/`OA`/`[B`/`OB` and nothing else, because Up and Down were the only
arrows bound when it was written -- so an untranslated `\x1b[C` fell through
to "unknown sequence, swallow" and the command column never moved. The shifted
pair was fine because its terminfo entry translates before the resolver sees
it.
Two fixes, not one: all four directions are in the table now, AND a test
asserts that every plain arrow the dispatcher binds is one the resolver can
produce. The first fixes this instance; the second is what would have caught
it by construction, and is what stops the next key from repeating it.
Measured rather than assumed: a pty announcing `xterm-256color` delivers
393 / 402 / 269 / 18 for Shift-Left, Shift-Right, F5 and Ctrl-R -- exactly the
keycodes bound. Live: the sort underline walks CPU% -> MEM% -> USER and back,
the command column scrolls, F5 is accepted and the TUI keeps running.
14 mutations, all caught.
Plan: docs/superpowers/plans/2026-09-23-glances-v5-tui-refresh-and-sort-arrows.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
`ENTER` prompts for a pattern, `E` erases it, `-f/--process-filter` sets it
from the command line, the three aggregate rows appear under a filtered
table, and `M` resets their min/max.
§8.1'S LAST OPEN QUESTION ANSWERED ITSELF, on two facts rather than a
judgement call. v4's own web UI has NO process filter -- grepping its
components for `process_filter` finds only help.vue, which documents the
TERMINAL key. And v5's TUI and REST API are mutually exclusive:
`main_v5.assemble` is `if args.server: … elif not no_tui: …`, so when the TUI
runs there is no FastAPI app in the process at all and the engine-global leak
the objection was about cannot happen. A filter in v5's browser would be a new
feature, not parity.
A THIRD POPUP TYPE, WITH A CANCEL. `curses.textpad.Textbox` -- which v4 wraps
in a `GlancesTextbox` subclass just to make Enter submit
(glances_curses.py:1426-1435) -- has no cancel at all, so a prompt opened by
accident has to be cleared by hand before you can get back to where you were.
ESC returns None and nothing is touched. The field is seeded with the current
pattern (editing a filter should not mean retyping it) and scrolls rather than
truncating when the pattern outgrows the box.
AN INVALID PATTERN IS REPORTED. `GlancesFilter`'s setter compiles the regex
and, on failure, quietly sets the filter back to None and writes a log line
(glances/filter.py:141-145). In v4 that makes a typo a keypress that does
nothing, with no feedback anywhere the user is looking -- the same defect
class b2 fixed for a refused renice. Comparing what went in against what came
back is the only way to tell "cleared" from "rejected" through that API.
THE MIN/MAX LIVE IN THE TUI, NOT THE RENDERER. v4 keeps them on the plugin
instance (`mmm_min`/`mmm_max`), which is what makes its renderer stateful;
v5's renderers are pure functions of (payload, fields, view). So the renderer
exports a pure `summarise()` and the memory across frames sits beside
`_cursor_max`. Changing or erasing the filter resets it: the extremes describe
a set of processes that no longer exists, and keeping them would make the next
filter start from the previous one's numbers.
The rows are declared to the vertical solver through the same
`process_extra_rows` the `e` block uses -- one function now, because the
solver wants one number and both blocks break its "one line per data row plus
one header" model.
`-f` is applied in TUI mode only. `glances_processes.process_filter` is global
to the process, so a server-wide filter would silently narrow what every REST
client sees; v4 refuses it outside standalone for the same reason
(main.py:150-152). On the command line there is no popup to show an invalid
pattern in, so it is logged instead.
THE PROMPT READS THROUGH `_read_key`, the escape-sequence resolver b1 added
for the main loop -- otherwise an untranslated arrow key, which arrives as a
bare 27 followed by its tail, would read as a cancel and throw away whatever
had been typed. Routing it there exposed that the peek was too greedy: it read
up to six bytes after any ESC, which in a text field means swallowing
characters the user typed and never saw. It now stops as soon as the tail
cannot be a sequence at all (only `[` and `O` can start one) or has reached
its final byte, and hands back the key it looked at. So Esc followed by a
letter is a real Esc AND keeps the letter, where before it was neither.
`M` without a filter now says so. v4 reads its reset flag INSIDE
`if process_filter is not None` (processlist/__init__.py:648-650), which is
why `M` could not ship before this chantier and why, in v4, it is a key that
does nothing and says nothing.
Verified live in a pty: the prompt appears, a pattern narrows the list, the
three rows render with min and max genuinely diverging from current across
frames (1.5/0.5/745M against 10.4/3.7/8.3G), `('M' to reset)` is there, a bad
pattern raises the report popup with the seeded text visible, and `E` clears
both. The engine's narrowing checked separately across all four pattern
shapes -- bare name, regex, `username:`, `cmdline:` -- 90 processes down to 1
or 89 as each should.
22 mutations, counting the six re-run on the resolver. Four survived the first round and every one was a real weakness:
a max-tracking test whose largest sample was last; a scrolling test typing one
repeated character, so head and tail were indistinguishable; an ESC test that
a hung loop would have satisfied; and a redundant `isinstance` beside the
`unknown` flag that made dropping the flag invisible -- simplified away rather
than tested around. All caught now.
Plan: docs/superpowers/plans/2026-09-23-glances-v5-tui-process-filter.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
A click on a process row pins it and shows the extended block above the
table; a button unpins it. v4's web UI already offers exactly this
(plugin-processlist.vue:59, :720, :726) and v5's browser had nothing.
FIRST, A CORRECTION TO WHAT I RECORDED. The maintainer's "TUI-only" ruling
was about `k`, `+` and `-` -- the keys that ACT on a process. I wrote it down
as covering all of 2.X-b, `e` included, which would have silently dropped a
v4 feature against the standing rule that a v4 feature is dropped only by an
explicit decision. §8.1 of the design and the §10 entry now record the ruling
per key: no for the three mutating ones, yes for `e`, and b4's filter still
open.
THE PAYLOAD RIDES IN /api/5/all, as plugin METADATA. `fetchAll` makes one
request per tick on purpose -- 34 components at a 2 s cadence would otherwise
be 17 req/s per tab -- so a per-tick GET was out for a feature that is off
almost all the time. `processlist._add_metadata` publishes `extended` the way
`fs` already publishes `free_space`. Not v4's shape: v4 merges the extended
values into the pinned list ITEM, which v5 cannot do without declaring twenty
fields that are null on every process but one, since `_remove_parameters`
filters each item to `fields_description`.
TWO POST ROUTES set the same `extended_pid` the TUI's `e` sets -- one pin, two
ways to ask for it. They are the only state-changing pair in an otherwise
read-only API, so the posture is stated in the code rather than left implicit:
an unauthenticated caller gains one pinned process' affinity, ionice, fd
count, swap and connection counts, of the same nature as the process list it
can already GET, and exactly v4's exposure. Nothing on the host is modified,
and under `[outputs] password` they sit behind the same middleware as every
other route. A pid absent from the published list is refused with 404, as v4
does -- v4 reaches that through `int(pid)`, which raises ValueError (500) on
garbage; FastAPI's path type answers 422 before the handler runs.
THE TWO SURFACES ARE HELD EQUAL BY A DRIFT TEST. process_extended.js mirrors
`_extended_rows` segment for segment, and the test compares the labels each
emits for the same payload -- the hotkeys.js pattern applied to a stats block.
So the browser shows v5's OWN terminal lines, including the IO nice line v4
never renders, rather than v4's narrower three.
TWO THINGS MEASURED RATHER THAN ASSUMED, both in a real browser.
The header stays "Command". v4's web UI says "Command (click to pin)", and I
shipped that first; in Chromium that column is the elastic remainder and lands
at 161px at 640, 72px at 900, 137px at 1280, while the wording needs 186. It
would have wrapped the header row at nearly every width. The affordance is
carried by `cursor: pointer` and a `title` instead, neither of which costs a
pixel of layout.
The block is titled with the process NAME, not its command line.
`glances_processes.extended_process` carries no `cmdline` at all -- the engine
adds it after the extended grab. v4's title works only because v4 reads the
published list ITEM instead.
A FIFTH v4 DEFECT, found by clicking in a browser rather than by a test: pin a
process, let it exit, and the block stays on screen showing its last numbers
forever, still flagged `extended_stats: True`. Nothing in the engine's update
loop runs again for a pid that has left the list, so `extended_process` is
never refreshed and never cleared. Fixed at the engine, where both surfaces
read it, and the PIN is dropped with the accumulator -- leaving it set would
keep the TUI's cursor frozen on a block it no longer draws.
AND A SIXTH THAT b3 MADE REACHABLE rather than introduced:
`__get_extended_memory_swap` caught NoSuchProcess/KeyError with a bare `pass`,
leaving `memory_swap` unbound -- so the `return` below raised
UnboundLocalError instead of reporting "no swap figure". Nothing but the `e`
path calls `set_extended_stats`, so no v5 release could reach it until now,
and the two ways in are issue #1551 and a process that exits mid-grab --
exactly what pinning a short-lived process does. One line, with the three
swap-read paths now pinned as one decision.
Verified end to end in Chromium at 420/640/900/1280/1600px: click pins, the
block renders its three lines, the pinned row's command is underlined
(computed style, not appearance), Unpin clears it, both POSTs fire, and the
page never overflows. The dead-pin fix confirmed against a real `sleep` that
was killed mid-pin.
9 mutations on the new guards, all caught; 7 more on the drift test, all
caught.
Plan: docs/superpowers/plans/2026-09-23-glances-v5-webui-process-extended.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
`e` pins the process under the cursor and shows v4's four-line block above
the table: the pinned name, CPU min/max/mean with affinity and IO nice, MEM
min/max/mean with the memory breakdown and swap, then the Open counters.
Press it again to unpin.
2.X-b stays TUI-ONLY -- the maintainer's decision, which closes §8.1 of the
design for b4 too. `k`/`+`/`-` would need a mutating REST endpoint on an API
Glances leaves unauthenticated by default, and the process filter is
engine-global: one typed in a browser tab would change what the TUI and every
other consumer sees.
Three things this chantier had to settle.
THE ENGINE IS GIVEN A PID, NOT A POSITION. v4 pushes its cursor position in
through `set_args`; v5 never hands the engine an argparse namespace, and a
position is the wrong handle regardless -- the list re-sorts every cycle, so
the block would describe a moving target. `extended_pid` is additive and None
unless the v5 TUI sets it, so v4's path is untouched. That absence of wiring,
not a gap in the key table, is why `e` did nothing in v5 before today:
`is_selected_extended_process` reads four attributes off `self.args`, and
with `args` unset the first `hasattr` makes it a constant False.
THE DATA REACHES THE RENDERER THROUGH THE PER-CYCLE `view`, read from the
engine by the TUI rather than fetched by the renderer. The renderers stay
pure functions of (payload, fields, view), and the extended stats stay out of
the REST payload entirely -- which is what TUI-only requires. A pid guard
drops a stale payload on the two cycles where it matters: right after `e`
(the engine has not grabbed yet) and right after the pin moves. Showing the
previous process' numbers under the new name is worse than showing nothing.
THE VERTICAL SOLVER IS TOLD WHAT THE BLOCK COSTS. `plan_right_column` models
an elastic block as "one line per data row plus one header"; four unmodelled
rows would overflow the body by four. The first attempt had the renderer
spend its own budget instead, which is wrong for a reason worth recording: a
four-row block does not fit in a budget of three, and truncating a stats
block is worse than useless. So the height is DECLARED
(`extended_block_height` -> `process_extra_rows`), derived from the rows
themselves rather than a constant. The WebUI's copy of the solver needs no
change: the parameter defaults to 0 and the browser has no `e`, which is what
keeps the drift test passing.
Two smaller divergences from v4. `e` may target Glances itself -- it only
LOOKS at the process, unlike `k`, and refusing would be the surprise rather
than the protection. And turning it off clears `extended_process`, which is
what actually stops the engine grabbing: the grab is keyed on that being set
(processes.py:663-669), not on `disable_extended_tag`. v4 keeps paying for it
after `e` is pressed again; only its renderer stops looking.
The cursor freezes while the block is up, as in v4 (glances_curses.py:356).
One thing v4 has no equivalent of: a terminal that SHRINKS can pull the clamp
down under a cursor that never moved, and then the pinned block and the
underlined row would name different processes with `k` following the
underline. The clamp re-pins, so what is described stays what is selected.
A FOURTH v4 DEFECT, found while reading the payload rather than the code:
`maybe_add_ionice_line` (processlist/__init__.py:728-742) guards on
`hasattr(prog['ionice'], 'ioclass')`, but the engine stores
`namedtuple_to_dict(proc)` (processes.py:669) -- psutil's `pionice` namedtuple
is a dict by then, and a dict has no `.ioclass`. The guard is always False, so
v4 NEVER RENDERS ITS IO NICE LINE. Checked against the live engine, not
inferred: it yields `{'ioclass': <IOPriority...: 0>, 'value': 0}`. v5 reads
the dict, and the line is there in the live smoke test.
Verified at three levels, because the pty screen reconstruction turned out
unreliable on ncurses' optimised diffs (the pyte lesson again): every string
of the block found in the raw terminal stream; three DOWN presses leaving the
pinned name untouched; and the frame-level check -- the authority -- printing
all four rows in order with real values.
17 mutations, 16 caught. The survivor replaces the derived height with the
constant 4, which is what it evaluates to today whatever the payload: an
equivalent mutant, not a test gap. The derivation still earns its place --
`test_the_declared_height_is_what_the_block_actually_renders` fails the day
the block grows a fifth line and a constant does not follow it.
Design: docs/superpowers/specs/2026-09-23-glances-v5-tui-process-management-design.md
Plan: docs/superpowers/plans/2026-09-23-glances-v5-tui-process-extended.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
UP/DOWN select a process, `k` kills it after asking, `+`/`-` renice it. With
`--disable-cursor` none of that exists, as in v4. The engine already carried
every action (processes.py:758, :769, :780); the whole chantier was on the
TUI side, which had no cursor and no popups of any kind.
2.X-b is designed as one group and SPLIT INTO FOUR, because the roadmap's
one-line "process management" is six features with three different
prerequisites. This is the first two. `e` (extended stats) and the process
filter -- ENTER/E, -f, the filtered summary, and `M`, which turns out to reset
only that summary and so cannot ship before it -- follow as b3 and b4.
Four decisions worth naming:
THE CURSOR IS CLAMPED TO THE ROWS THE LAST FRAME DREW, not to
glances_processes.processes_count -- which reads 0 in v5, because
_max_processes is never set (processes.py:247-254). The renderer is the
authority; only it knows what the vertical-fit pass left room for. The
property this buys is what makes `k` trustworthy: the selection is always on
screen, so a confirmation can never name a process the user cannot see.
A MUTATION NAMES ITS TARGET BEFORE IT ACTS. The pid is captured from the
frame on screen, once, before any popup, and the engine is called with that
captured value -- never re-resolved through the cursor afterwards. The list
re-sorts every cycle, so an index resolved after a confirmation can address a
different process than the one the confirmation named.
_handle_key STAYS PURE. A popup is curses I/O, so a key that needs one
returns a new result kind, "modal", and _loop runs it. Same separation v4
reaches by accident (handler sets a flag, display() draws the popup), made
deliberate -- and it is what keeps the dispatch tests running without a
terminal.
A REFUSED RENICE IS NOW VISIBLE. v4 logs it and shows nothing
(processes.py:767, :778), so pressing `+` on someone else's process does
literally nothing a TUI user can perceive. nice_increase/nice_decrease now
return whether the OS accepted; v4's callers ignore the return, so the change
is additive.
A DEFECT THE LIVE SMOKE TEST FOUND, which the design had not predicted:
pressing Down EXITED GLANCES. ncurses translates whichever arrow encoding its
terminfo lists -- xterm's kcud1 is the application-mode \x1bOB -- and hands
the other back byte by byte: 27, 91, 66. 27 is quit. This was never only
about arrows; a mouse report, a bracketed paste or an unmapped function key
is ESC-prefixed too, and each of them quit Glances. Binding the arrows is
what turns it from a curiosity into the first thing a user hits. `_read_key`
peeks once, non-blocking, behind a 27: nothing there is a real Esc; something
there is a sequence to resolve or swallow. It also fixes, for free, arrows
scrolling the help overlay instead of closing it.
Three v4 defects surfaced and recorded in the design rather than fixed here:
1. k/+/- read the pid from the processlist plugin unconditionally
(glances_curses.py:638, :641, :647) -- even while the PROGRAM list is on
screen, so they act on a row other than the one displayed. v5 refuses in
program view; its programlist schema carries no pid at all.
2. v4 runs both of its dispatch tables on every keypress
(glances_curses.py:303-305), and the second binds the raw codes 65/66 as
arrow fallbacks -- which are ord("A") and ord("B"). Pressing `A` in v4
toggles AMPs AND moves the cursor. v5 does not carry the aliases.
3. `M` reads its flag inside `if process_filter is not None`
(processlist/__init__.py:648-650), so with no filter it does nothing.
Verified live in a pty, not only in tests: the underline follows the arrows
and is absent under --disable-cursor (0 command underlines vs 9); `+` moved a
real process from nice 0 to 1; `k` drew "Kill claude (pid 104)? Confirm
([y]es/[n]o):" and `n` killed nothing; `j` then `k` refused with the program-
view message; and the own-pid guard fired on the TUI's own process, which is
exactly where a CPU-sorted list puts it.
Every new guard is mutation-tested (23 mutations, all caught), including one
that only a loop-level test catches: bypassing _read_key in _loop, which is
precisely the shape the original defect had.
docs/cmds.rst needs no change -- it already documents all five keys, because
it describes v4's set and v5 is catching up to it. test_routes_v5's frozen
argument set does: --disable-cursor is a boolean display preference like
--disable-unicode beside it, nothing sensitive, so /api/5/args carries it.
Design: docs/superpowers/specs/2026-09-23-glances-v5-tui-process-management-design.md
Plan: docs/superpowers/plans/2026-09-23-glances-v5-tui-process-cursor-actions.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
Three more of 2.X-c, on both surfaces. Unlike part 1's `b`/`6`/`F`, v5
rendered only ONE mode of each of these -- but the data for the second was
already collected, so each is a renderer change, not a model change.
`B` disk I/O byte/s vs IOPS. `read_count`/`write_count` were already
collected and flagged `internal` (out of the generic renderer's default
columns, never out of the payload). They now carry v4's own `IOR/s`/`IOW/s`
short names, so swapping the field pair swaps the header with it on both
surfaces -- the labels come from the schema. IOPS need their own formatter:
counts scale by 1000 and carry no unit, where bytes scale by 1024 and carry a
`B`. `_format_count_rate` and `formatIops` are that pair, deliberately not
`formatCount` (1024-based).
`0` load average vs Irix percentage (issue #1554). `cpucore` was already
collected and `internal`. v4 reads the count from the `core` plugin's
`log_core()`; v5 reads the field the load payload already carries -- same
number, no cross-plugin reach. Both surfaces keep v4's guard: an absent or
zero core count falls back to the plain float rather than dividing by zero.
The key changes what the cell DISPLAYS, never which level colours it --
per-core normalisation is already implicit in the thresholds
(`normalize_by: cpucore`), and a test pins that.
`T` network Rx/Tx apart vs combined. A divergence in route, not in value: v4
renders a `bytes_all` field its model computes; v5's schema has no such
field, so both renderers sum the two rates they already carry. The sum of two
rates over one interval IS the combined rate. The combined cell takes no
threshold colour -- the two fields own their levels and a sum belongs to
neither, which is why v4 paints its own combined cell plain.
The diskio render fixture gained `read_count`/`write_count` on every row and
their short names. It predated the fields, so under `B` every row was dropped
for a missing value and the header fell back to the raw field names. A
fixture that does not carry what the server sends tests nothing.
Verified against real plugins: `B` turns `R/s 0B` into `IOR/s 0`, `0` turns
`1 min 0.46` into `1 min 11.6%`, and `T` turns `642b 642b` into `1.3Kb`.
2.X-c now stands at six of nine. The remaining three are blocked on data, not
on keys: `U` needs the raw counter `_transform_gauge` replaces with its rate,
`L` needs `read_time`/`write_time` collected at all, and `S` needs a v5
history store. Section 10 records each with its file reference.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
Settles the question the SHOW/HIDE commit left open. These four keys do not
remove a block -- they flip HOW something is shown, and each has a server-side
default in /api/5/args, so binding them meant deciding whether a browser
keystroke may override server state.
It may, per key, for that viewer only. `AppShell.viewOverrides` holds an entry
only for a key that was actually pressed; every other flag keeps following
`serverArgs`, which is re-read on every tick. A copy of `serverArgs` seeded at
mount would have been silently overwritten by the next poll, and would have
frozen a viewer who never pressed the key.
The override flips the EFFECTIVE value, not a `false` default. That is what
makes the first press of `4` on a server started with `--full-quicklook` turn
the mode off instead of doing nothing -- two tests pin it against the
`cpu-percpu-on` and `quicklook-full` fixtures, which ship those flags on.
The merged result is published as `effectiveArgs` and passed down under the
existing `server-args` prop, so PluginPercpu, PluginQuicklook and the two
process blocks pick the toggles up with no change to any of them. The prop's
meaning becomes "the effective view flags", documented at its computed;
renaming it would touch all 32 components for no behavioural gain.
`/` was not merely unbound, it was unimplemented: `commandText()` hardcoded
`short_name=True` and said so ("the only mode either WebUI block has"). It now
takes the flag, `_split_cmdline` is mirrored as `splitCmdline()`, and the
wrapper lives in the shared process_block.js mixin so both templates keep
calling `commandText(item)` unchanged.
That one carries a divergence the browser cannot avoid: the TUI shows the path
prefix only when `os.path.isdir(path)` holds (render_curses_v5.py:329), and a
browser has no filesystem. The two disagree only for a `cmdline[0]` that looks
like a path but is not one. Documented at the function.
hotkeys.js gains VIEW_KEYS, mirrored from the TUI's TOGGLE VIEW group with its
descriptions, so the drift guard now covers both families -- plus a check that
every entry names the flag it overrides, since one without a flag would be a
key that dispatches and does nothing. The help overlay groups the two families
the way the TUI's `_HELP_GROUPS` does, TOGGLE VIEW first.
11 tests added across the three levels. Starting the override from `false`
instead of the effective value fails 3 of them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
The browser now binds the same keys the TUI got in 2.X-a, reversing section 6.5
of that design ("the WebUI is not affected") at the maintainer's request.
The two surfaces keep independent hidden sets -- two viewers, two states, and
`ViewState` is still something the REST layer never exposes. What they share is
the TABLE: js/v5/hotkeys.js mirrors the `hide` entries of `_HOTKEYS`, targets
AND descriptions, so the two help screens cannot end up describing the same key
differently. tests/test_webui_v5_hotkeys_drift.py fails on any divergence --
the same convention full_quicklook.js and degrade.js already follow, and the
file is generated from the Python table rather than transcribed.
The two slot keys (`2`, `5`) are not a second copy of the slot lists: they
resolve against the live plugin registry at press time, so they cover exactly
the plugins the page actually renders and cannot drift from the TUI's slots.
That also means `2` then `n` un-hides network alone, matching the TUI.
`AppShell.slots()` unions the viewer's set in exactly where `build_frame`
unions `user_hidden`, so the composition rule is the same on both sides -- a
block is hidden when either authority says so, and a key cannot conjure width
the degradation cascade has already taken away.
`h` opens an overlay built from the same table, so a bound key cannot go
undocumented -- the property the TUI gets from generating its overlay out of
`_HOTKEYS`. It is `v-if`, not `v-show`: a closed overlay must not sit in the
DOM where the cascade would measure it.
The listener sits on `document`, not on the root element, because the page has
focusable controls (the refresh steppers) and a component listener would stop
working the moment one took focus. It ignores any keystroke carrying a
modifier, so browser shortcuts keep working, and anything typed into a field.
Not bound here: the TOGGLE VIEW keys (`1`, `j`, `4`, `/`). The browser derives
those from `serverArgs` today, and binding them needs a decision on whether a
browser keystroke may override server state. The stale comment in `slots()`
claiming browser hotkeys are out of scope is corrected to say this.
Testing, at the three levels the behaviour has: the drift guard above, JS unit
tests for the toggle logic (including that a compound key never lands
half-hidden), and render-probe tests proving a key actually removes the block
from the DOM. The probe gained an optional key-sequence argument -- its fake
document has a no-op addEventListener and cannot dispatch a real keydown, so
the keys go through the same method the listener calls. Dropping the union in
`slots()` fails 6 of them.
hotkeys.js follows the tabs + double-quote style of its siblings in js/v5/,
not the repository's .prettierrc (4-space, single-quote), which no v5 source
follows -- AppShell.vue already fails `prettier --check` on HEAD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
Follow-up to the stderr logging fix. Reproducing v4 exactly left `-s` silent:
v4 pins the console handler to CRITICAL in every mode, so an operator running
a foreground server -- or reading `docker logs` -- saw nothing at all, the
unauthenticated-API warning and the bind address included.
`setup_logging` takes a `server` flag. Only the TUI needs the terminal kept
clean, because there stderr IS the surface curses paints; `-s` never starts
one, so its console carries the normal log (INFO, or DEBUG under `--debug`).
A deliberate v4 divergence, recorded in section 4.7.
`--quiet` / `--no-tui` also runs without curses but keeps the quiet console
its name promises. Only `-s` opts into the verbose one.
Server mode also promotes the console to v4's own `standard` formatter. v4's
`console` handler carries a bare `%(message)s`, which is right for the
CRITICAL death-rattle it normally is but drops the severity from a stream an
operator now reads as a log. The format is taken from v4's LOGGING_CFG rather
than invented here.
One subtlety the tests pin: the console handler object is shared across calls,
so a server run followed by a TUI run in the same process must not leave the
console verbose. `glances_logger()` re-applies the whole config first, which
resets it.
Verified live in all three modes: `-s` prints the log with severities, the TUI
display carries no log text at all, and `--quiet` writes nothing to stderr.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
Found while smoke-testing the SHOW/HIDE hotkeys: every WARNING scrolled the
curses display and desynchronised ncurses' model of it, so text bled across
columns and stayed corrupted. It made two smoke runs unreadable until the
child's stderr was redirected to a file.
`setup_logging()` called `logging.basicConfig(force=True)`, whose default
handler writes every INFO and WARNING to stderr. The irony is that v4's
configuration was already installed and correct -- `glances.logger` is
imported transitively by shared v4 modules, and by the time `setup_logging`
ran the root logger already carried a RotatingFileHandler at DEBUG and a
stderr handler pinned at CRITICAL. `force=True` tore both down.
So the fix is to stop destroying it: `setup_logging` now calls
`glances_logger()` and sets the root level. v4 stays the single source of
truth for the file location (XDG-aware, $LOG_CFG override, 1 MB x 3 rotation)
and for the CRITICAL console threshold -- nothing is duplicated here.
`dictConfig` replaces the root handler list outright, so this keeps what
`force=True` was there for: a root handler attached by pytest or any embedding
harness after import is still dropped, and calling it twice does not stack
handlers. Both are now tested.
Behaviour, matching v4 exactly and verified end to end:
- WARNING and INFO reach the log file, never the terminal.
- CRITICAL still reaches the terminal -- which is what the threshold is for:
every fatal path in main_v5.py logs at `critical` before sys.exit(2), so an
unloadable config still prints its reason instead of exiting mutely.
- `--debug` raises the ROOT level, making the FILE verbose. It does not lower
the console threshold: debugging must not make the TUI unusable.
5 tests added. The three that pin the console threshold and the rotating file
all fail when the old basicConfig call is put back.
Live check at 160 columns with stderr NOT redirected -- the case that
originally broke -- shows a clean display and no log text anywhere on it.
Recorded in section 4.7 of glances-v5-architecture-decisions.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
Reported from a TUI smoke test: pressing `4` left LOAD on the row. The
behaviour was in fact exact v4 parity -- `enable_fullquicklook`
(glances/outputs/glances_curses.py:451-455) disables cpu/npu/mpp/gpu/mem/memswap
and leaves `load` and `percpu` -- but the mode's own help text has always
advertised the wider behaviour, and leaving two blocks behind made `4` read as
an arbitrary subset rather than a mode.
Maintainer's call: `4` means quicklook alone, full width. Every TOP sibling now
goes, `load` and `percpu` included. A deliberate v4 divergence, recorded in
section 10 of glances-v5-architecture-decisions.md under "Reversed decision --
full quicklook" and in the cell of the parity inventory that claimed the sets
still matched.
`_FULL_QUICKLOOK_HIDDEN` is derived from `TOP_SLOT` instead of being listed, so
a TOP plugin added later is covered without a second edit. The WebUI's mirror
of it (full_quicklook.js) follows -- its drift guard is what caught the copy,
and the guard now counts against `TOP_SLOT` rather than a literal 6.
The help text and the `_handle_key` comment both described a hidden set that
was wrong before this change and would have been wrong after it; both now say
what the mode does. `_fit_full_quicklook`'s docstring described siblings that
no longer exist; the function still earns its keep (it measures the real
label/bracket overhead instead of assuming 8) and says so.
Four tests encoded the old decision and are rewritten, not deleted. The
overflow guard among them -- `test_full_quicklook_leaves_room_for_load`, which
existed because the bars used to be sized at a flat `max_x - 8` and pushed LOAD
off screen -- keeps its real purpose: the sibling it protected is gone, the
overflow is not, so it now asserts quicklook is exactly the row width.
Verified live at 160 columns: the row holds quicklook alone, bars spanning the
full width.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
Closes gap #2 of the v4 -> v5 parity inventory: v5 handled 15 of v4's 62
hotkeys and had no per-plugin visibility keys at all. It now handles the
whole SHOW/HIDE family.
A cloud-less amps C cloud d diskio D containers f fs+folders
G gpu I ip K connections l alert n network N now P ports
Q irq r smart R raid s sensors V vms W wifi z processes
7 npu 8 mpp 2 left sidebar 3 quicklook 5 top row
Mechanism: a fourth `_HOTKEYS` action kind, `hide`, whose value is always a
tuple of plugin names, so a compound key (`f` -> fs+folders, `z` -> the three
process blocks) and a slot-wide key (`2`, `5`) are not special cases. The
tuple flips as a unit keyed on its first member, so a compound key can never
land half-hidden. State lives in `ViewState.hidden_plugins`.
The renderer now hides a block on the UNION of two authorities: the user's
set, and the `hide_<plugin>` keys the width-degradation cascades write. They
are deliberately separate namespaces -- the cascade rebuilds its keys every
cycle, so a user choice stored there would be clobbered by the next fit, and a
user-hidden block would make the cascade believe it had already spent that
step. Reading both also collapsed the seven hardcoded per-plugin `if`
statements in `build_frame` into one `hide_{plugin_name}` lookup.
The count in the roadmap said 23. It is 24: `C` (disable_cloud) is missing
from Part 3 of the parity inventory entirely, and `r` (disable_smart) is filed
there under MISCELLANEOUS though it is a visibility toggle. Conversely `e`
(extended stats) is listed under SHOW/HIDE but needs the process cursor, so it
stays with 2.X-b. Part 3 is not amended here -- it is a dated snapshot and
deserves its own pass. Roadmap section 10 is corrected.
Three divergences from v4, all forced by v5's architecture and all release-note
material:
- A config-disabled plugin is never instantiated in v5, so its hotkey is a
visible no-op. This is most apparent on `Q`/irq, which is opt-in in v4.
- `z` hides the process blocks but does not stop the shared `glances_processes`
engine: v5 shares it with the REST API and the WebUI, and a TUI keypress must
not blank /api/5/processlist for every other consumer.
- `2` is expanded to its slot members, so `2` then `n` then `2` leaves network
visible. v4's own `5` already behaves this way.
`r` follows the v4 code (disable_smart), not the v4 docs, which mislabel it
"Reset history" -- a v4 documentation bug, untouched here.
46 tests added, including a 24-case key sweep, a drift guard that every `hide`
tuple names a real slot member, and one case per cascade key so the collapsed
`if` chain is proven equivalent rather than merely plausible. Three mutation
runs confirm the new tests bite. Verified live against real plugins, both
through a pty and headlessly at frame level.
Design: docs/superpowers/specs/2026-09-21-glances-v5-tui-show-hide-toggles-design.md
Plan: docs/superpowers/plans/2026-09-21-glances-v5-tui-show-hide-toggles.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
Add end-to-end test asserting that uptime/system/now appear in the TUI
registry while core/version/psutilversion are discovered (REST-served)
but filtered out of TUI rendering. Add system/uptime/now sections to
the tui-v4-rendering-patterns.md catalogue with v4 layout notes and v5
source pointers.
Last MCP gap closure. Both plugins reuse the v4 glances_processes
singleton (no engine rewrite — strategy two-phase): processcount calls
engine.update() + get_count() each cycle, processlist consumes the
pre-sorted list via get_list(). KNOWN_V5_MISSING_PLUGINS shrinks to ().
- processcount: scalar with total / running / sleeping / thread /
pid_max; TUI mirrors v4's "TASKS N (M thr), R run, S slp, O oth"
header.
- processlist: collection PK=pid; minimal column set CPU% / MEM% / PID /
USER / THR / NI / S / Command, top-20 rows. cpu_percent and
memory_percent are watched (50/70/90, prominent=False — parity fs).
- Engine-internal fields (memory_info, cpu_times, io_counters, gids,
time_since_update, key) flagged internal=True so MCP/export keep
them but the generic TUI skips them.
- Out of scope (deferred to G5 with args/config plumbing): extended
view, programs aggregation, filter UI, interactive sort.
41 new tests (14 model + 27 renderer), v4 catalogue updated, MCP gap
log + adapter docstring updated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Last entry of ``KNOWN_V5_MISSING_PLUGINS`` after this commit:
``("processlist",)``.
Model (``glances/plugins/diskio/model_v5.py``):
- Fields: disk_name (PK, string), read_count / write_count
(rate, number, internal — exportable for IOPS consumers but
not rendered), read_bytes / write_bytes (rate, bytespers, watched,
``prominent=False``, ``strict_thresholds=True``, NO default
thresholds).
- Sustained disk traffic is host-specific (a DB server may stream
MB/s by design) — alerts only fire when operators set
``read_bytes_warning=...`` per-disk or per-field in
``[diskio]``. ``strict_thresholds=True`` blocks the bare-``<level>``
fallback (same pattern as memswap.sin/sout) so a legacy
``[diskio] careful=50`` cannot trigger spurious alerts.
- ``read_time``/``write_time`` and the derived ``read_latency`` /
``write_latency`` of v4 are not ported — deferred with the
``--diskio-latency`` mode.
- ``psutil.disk_io_counters()`` may raise or return ``None`` on
platforms without disk I/O support — model returns ``[]`` rather
than crashing.
Renderer (``glances/plugins/diskio/render_curses_v5.py``):
DISK I/O R/s W/s
nvme0n1 0B 0B
sda 1.4M 732K
- 3-cell rows, 18 + 1 + 7 + 1 + 7 = 34 chars (fits sidebar cap).
- Sorted by disk_name. Cycle-1 disks (no rate baseline) are skipped
entirely — no ``-`` placeholder wall on startup.
- Rate cells display ``auto_unit(bytes_per_sec)`` WITHOUT a trailing
``/s`` — header carries the per-second semantic (v4 parity).
- Long disk names tail-truncated with leading underscore.
Adjacent:
- ``KNOWN_V5_MISSING_PLUGINS`` shrinks to ``("processlist",)``.
- ``test_attach_mcp_logs_known_v5_gaps`` updated.
- v4 catalogue grows a ``## diskio`` section + ✅ footer.
28 new tests (13 model + 15 renderer). Full v5 suite: 762 passed.
Sister of the v5 ``mem`` plugin. Same pattern, slimmer layout
(single-column body — v4 ``memswap.msg_curse`` does not 2-col).
Model (``glances/plugins/memswap/model_v5.py``):
- ``total`` / ``used`` / ``free`` — bytes, snapshot.
- ``percent`` — watched + prominent, default thresholds 50/70/90
(same ladder as ``mem`` for UX consistency).
- ``sin`` / ``sout`` — cumulative in v4; v5 exposes them as
bytes/sec via ``rate: True``.
- Tolerates platforms without a swap file (Illumos, OpenBSD —
issues #1767, #2719): psutil raises, model returns ``{}`` so
the scheduler tick keeps going.
Renderer (``glances/plugins/memswap/render_curses_v5.py``):
SWAP 25.0%
total 16.0G
used 4.0G
free 12.0G
- Line 1: ``SWAP`` (HEADER) + percent cell coloured by ``_levels.percent``.
Title escalates to warning/critical when the prominent percent reaches
those levels.
- Lines 2-4: ``total`` / ``used`` / ``free`` as label/value pairs.
- Value column floored at 6 chars so it does not jiggle between cycles.
Adjacent changes:
- ``KNOWN_V5_MISSING_PLUGINS`` in ``mcp_adapter_v5`` shrinks to
``processlist, fs, diskio`` — memswap no longer surfaces in the
MCP startup gap log.
- v4 catalogue (``docs/architecture/tui-v4-rendering-patterns.md``)
grows a ``## memswap`` section + ✅ footer pointing to the new
renderer.
22 new tests (11 model + 11 renderer). Full v5 suite: 669 passed
(+22), lint clean.
- ``conf/glances.conf``: add a commented ``[outputs] enable_mcp``
entry above the existing ``mcp_path`` / ``mcp_allowed_hosts`` keys.
Notes that the gate is off by default and that ``--enable-mcp``
flips it via the config overlay.
- ``docs/architecture/glances-v5-architecture-decisions.md``: new
§11 "MCP endpoint" covering:
- §11.1 opt-in lifecycle (CLI + config)
- §11.2 adapter architecture + flow diagram
- §11.3 resource/prompt inventory with v5 status per entry
- §11.4 known v5 gaps (logged on mount)
- §11.5 alert schema (v5-native, no v4 translation — decision
logged in the G3-MCP plan)
- §11.6 auth (HTTP middleware passes SSE through; no special MCP
middleware needed)
- §11.7 DNS rebinding (independent ``mcp_allowed_hosts``)
- §11.8 out of scope (history buffer, unported v4 plugins,
WebSocket transport)
- New §1.5 "Mode dispatch (CLI ↔ runtime)": alignment table, ASCII
diagram of the assemble/serve branching, rationale (v4 mental model
+ remove unauthenticated default footgun), and open points (fate of
--quiet/--no-tui, client mode).
- §1.4 (TUI thread): rephrased "CLI control" + "Shutdown" bullets to
reflect that default mode no longer starts uvicorn.
- §4 (REST API): preamble notes the API server is now opt-in via -s;
every subsection below applies to server mode exclusively.
MCP wiring is referenced as deferred to a dedicated G3-MCP plan
(consistent with Task 2 of G2 being dropped).
- Add a "✅ v5 renderer at ..." footer line under each migrated plugin
section of the v4 TUI rendering catalogue (cpu, mem, load, network,
percpu). Network + percpu footers also note which v4 modes are
deferred to G2+ (--byte / --network-cumul / --network-sum and the
quicklook-enabled toggle).
- New NEWS.rst entry for ``5.0.0a3 (Phase 2 G1)`` summarising the
per-plugin renderer convention, the discovery mechanism, the new
schema renderer hints (short_name / internal), and the visual-parity
groundwork (prominent reverse pairs, 3-cycle alert warmup, top-row
spacing, CPU/perCPU toggle, dynamic title color, psutil baseline guard).
Plugins can declare an optional short_name in fields_description for
compact label display in tight per-plugin renderers:
ctx_switches.short_name = 'ctx_sw'
soft_interrupts.short_name = 'sw_int'
interrupts.short_name = 'inter'
A new field_label(schema, field_name, prefer_short=False) helper
encapsulates the resolution order:
- prefer_short=True: short_name -> label -> field name
- prefer_short=False (default, generic renderer): label -> field name
The cpu render_curses_v5 now pulls every column label via
field_label(..., prefer_short=True) instead of hardcoding labels.
Mirrors v4 short_name (cf. curse_add_stat in plugins/plugin/model.py).
Documented in architecture decisions section 3.2 and SKILL-plugin.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SKILL-plugin.md: new section explaining when and how to write a
per-plugin TUI renderer; pointers to cpu reference + v4 catalogue.
- architecture §1.4: per-plugin renderer documented as the escape hatch
for layouts the generic table fallback cannot produce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two TUI corrections raised by the v4/v5 visual comparison:
1. Internal fields (time_since_update, cpucore) leaked into the UI. They
are computation support (rate divisor, threshold normalizer) and
should never be displayed. Introduces a new `internal: True` flag in
the `fields_description` schema. Tagged:
- time_since_update (base class, every plugin)
- cpucore (cpu, load)
The flag is rendering-only: the field still goes through the REST
API and is available to `normalize_by` / `rate` computations.
2. Plugin blocks rendered as left/right cells with a fixed 1-space gap,
which made cpu/mem/load blocks misaligned. Now:
- scalar blocks: 2-column table (label left-aligned, value right-aligned)
- collection blocks: N-column table (primary key left-aligned, rest right)
Column widths auto-fit the widest content per block.
Docs:
- SKILL-plugin.md: `internal` added to renderer-hints table
- glances-v5-architecture-decisions.md §3.2: `internal` documented
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>