Skip to content

feat(core-status): let the Problems tab act on what it is showing - #448

Merged
guyverino merged 2 commits into
mainfrom
feat/core-status-problem-actions
Sep 7, 2026
Merged

feat(core-status): let the Problems tab act on what it is showing#448
guyverino merged 2 commits into
mainfrom
feat/core-status-problem-actions

Conversation

@guyverino

@guyverino guyverino commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

What & why

MoonBot's own Problems window carries "reset all" and "refresh". This tab had
neither. It had a channel test and a clear, both gated on the operator having
hand-picked exactly one core in the panel's core selector — a gate that is
permanently shut on every workspace-owned panel, because Auto pins its scope and
renders that selector read-only, so the retained Classic pick can neither be set
nor cleared there. Both buttons therefore read as always greyed, which is how
this started.

Three buttons now, and ONE rule behind all of them. A clicked finding narrows
them to its core; no click means the panel's whole scope. That is this panel's own
convention for a bulk command — the same "the selection when the operator has made
one, the displayed scope otherwise" the footer's fleet update already obeys. The
scope itself is still set where it always was, the core selector in Classic and the
workspace in Auto, so no button carries a private notion of which cores it means.

Refresh is not decoration, and the reason is not obvious. The protocol has no
request for a diagnostics list: a core pushes one on connection and again when its
detectors change it, docs/problems.md says outright that "there is no periodic
diagnostic-list refresh", and the only client command that yields a fresh list is
the destructive clear. But moonproto mutates its retained problems through three
paths and only two raise an event this feed can follow. The third is the
hard-session reset — on a ServerToken or peer-app-token CHANGE, i.e. a hard
reconnect or the MoonBot process restarting behind the same endpoint,
events/active.rs assigns settings.problems = Default::default() and publishes
nothing. The terminal never respawns its feed thread for that, so its store keeps
serving the findings of a core instance that no longer exists, with supported
still true. A restarted core normally pushes a list within seconds and repairs it;
a core too old for the extension never does. CoreCmd::RefreshProblems re-reads
what the library holds now and publishes it — usually identical and dropped by the
store, and after a silent reset the honest "this core has told this connection
nothing".

Notable decisions

  • The pick is a CORE, never the clicked row index. The finding list is rebuilt
    from live core data on every repaint, so an index outlives the row it named: one
    finding appearing or clearing above it re-points it at a different core, and the
    reset it narrows cannot be undone. The click resolves its index against the list
    it was drawn from, while that list is still the one on screen. Clicking any row of
    the picked core clears the pick — a narrowing that cannot be undone would strand
    the operator on one core.
  • The table keeps its DEFAULT selection mode. Taking the click through
    controlled_row_selection also hands the caller the row highlight and returns
    early from the table's entire keyboard block (data_table.rs:1100), which costs
    up/down/home/end navigation in a table that is read far more than it is clicked.
    The highlight is set per row from the picked core instead.
  • "Reset", not MoonBot's "Reset all". Their window belongs to one core, so
    "all" there means all of that core's findings. This tab spans a fleet, where the
    same word reads as "all cores" — a lie the moment the operator has clicked one.
  • The blast radius is stated before the press, not after. The tooltip carries
    the count, the confirm names the cores through the same non-truncating list the
    footer's fleet confirm uses, and the outcome reports the shortfall — "reached 9 of
    12" rather than a success toast over a partial result.
  • A pick is dropped the moment the surface can no longer show it. Clicking a row
    of its core is the only gesture that clears one, so a pick whose findings have all
    gone — reset on the core, or supported flipped back to unknown — would sit there
    narrowing an irreversible action with nothing on screen saying so. It is checked
    against the DRAWN rows, so the 500-row cap cannot strand one either.
  • The table is left with no selection of its own. Its row and cell cursors are
    cleared on every pick change: left behind they paint a second highlight from a raw
    row index (data_table.rs:1231, and the cell background at :1262 paints whether
    or not cells are selectable), and an index is exactly what the pick exists to
    avoid. select_row(None) alone is not enough — it leaves selected_cell behind.
  • Two refusals, not one. "The core you clicked is down" and "nothing in this
    scope is connected" have different remedies, and a scope full of live cores must
    never be reported as offline because one picked core is not.
  • The request joins the event gate rather than branching beside it. Both reasons
    read the same retained state through the same projection, so
    convert::snapshot_when carries the chain settings_event_snapshot already used
    and the tenth publish block in that loop is not a copy of the other nine.

Known limitations

  • The synthetic core swallows both new commands (feed/synth.rs has a catch-all),
    so the tab is inert under --fixture. Nothing there reports diagnostics anyway.
  • A reset reaches only the connected cores of a scope. The confirm's count states
    how many that is, but its text does not spell out that offline members were
    dropped.
  • Escape is deliberately left alone: it already carries a window-level meaning here
    (it closes an open chart), and a second one on the same key risks swallowing that.
    The pick is cleared by clicking its row again.
  • A refused reset or re-read reaches only the log; no strategy or diagnostics
    command has a result path back to the window today.

How to verify

cargo build -p moon-ui-gpui --bin moonterminal --target x86_64-pc-windows-msvc --all-targets
cargo fmt --all -- --check
cargo clippy -p moon-ui-gpui --bin moonterminal --target x86_64-pc-windows-msvc --all-targets -- -D warnings
cargo test --workspace --target x86_64-pc-windows-msvc

Green on the rebased tree: moon-core 1654, moon-ui-gpui 1500 + 320
(theme_contract), fmt clean. Clippy reports 78 findings on this tree and none in
any file this change touches
too_many_arguments on drain_commands already
fired at 12 arguments on origin/main, verified against the base file. FireTest not
run: this change touches neither chart, render, windows nor input.

By hand, on a fleet whose cores report diagnostics: click a finding and confirm all
three buttons narrow to its core and its rows highlight; click it again to widen
back to the scope; check the arrow keys still walk the table; reset one core and
watch its rows go when the core's next list arrives.

The protocol has no request for a diagnostics list. A core pushes one on
connection and again whenever its own detectors change it; moonproto's
docs/problems.md says outright that "there is no periodic diagnostic-list
refresh", and the only client command that produces a fresh list is the
destructive clear.

That would make a refresh button pure theatre, except for one thing the
library does silently. moonproto mutates its retained problems through three
paths and only two raise an event this feed can follow. The third is the
hard-session reset: on a ServerToken or peer-app-token CHANGE - a hard
reconnect, or the MoonBot process restarting behind the same endpoint -
events/active.rs assigns `settings.problems = Default::default()` and publishes
nothing. The terminal never respawns its feed thread for that, so its own store
is not rebuilt either and keeps serving the findings of a core instance that no
longer exists, with `supported` still true. The restarted core normally pushes a
list within seconds and repairs it; a core too old for the extension never does,
and the stale rows then stand forever.

`CoreCmd::RefreshProblems` re-reads what moonproto holds now and publishes it.
Usually that equals what the store already has and the store drops it. After a
silent reset it does not, and the honest answer - this core has told this
connection nothing - replaces the previous instance's findings, which is the
same rule `CoreData::begin_connection_attempt` applies when the terminal itself
reconnects a core.

The request joins the existing event gate rather than branching beside it: both
reasons read the same retained state through the same projection, so one gate is
all the difference between them. `convert::snapshot_when` carries the
snapshot/flatten/project chain that `settings_event_snapshot` already used, so
the tenth publish block in that loop is not a hand-rolled copy of the other
nine.

`clear_core_problems_many` and `refresh_core_problems_many` follow
`set_auto_detect_many`: one command per core, because that is what the protocol
offers, and the cores whose channel accepted are returned so a caller can report
what it actually reached.
MoonBot's own Problems window carries "reset all" and "refresh". This tab had
neither: it had a channel test and a clear, both gated on the operator having
hand-picked exactly one core in the panel's core selector. That gate was
permanently shut on every workspace-owned panel - Auto pins its scope and
renders the selector read-only, so the retained Classic pick can never be set or
cleared there - which is why both buttons read as always greyed.

Three buttons now, and ONE rule behind all of them: a clicked finding narrows
them to its core, and no click means the panel's whole scope. That is this
panel's own convention for a bulk command, the same "the selection when the
operator has made one, the displayed scope otherwise" the footer's fleet update
already obeys. The scope itself is set where it always was - the core selector
in Classic, the workspace in Auto - so no button carries a private notion of
which cores it means any more.

The pick is stored as a CORE, never as the clicked row index. The finding list
is rebuilt from live core data on every repaint, so an index outlives the row it
named: one finding appearing or clearing above it re-points it at a different
core, and the reset it narrows cannot be undone. The click resolves its index
against the list it was drawn from, while that list is still the one on screen.
Clicking any row of the picked core clears the pick, because a narrowing that
cannot be undone strands the operator on one core.

The table keeps its default selection mode on purpose. Taking the click through
`controlled_row_selection` also hands the caller the row highlight and returns
early from the table's entire keyboard block, which would cost up/down/home/end
navigation in a table that is read far more than it is clicked.

Reset is labelled "Reset", not MoonBot's "Reset all": their window belongs to
one core, so "all" there means all of that core's findings, while this tab spans
a fleet and the same word would read as "all cores" - a lie the moment the
operator has clicked one. The tooltip and the confirm both state how many cores
the press will reach, the confirm names them through the same non-truncating
list the footer's fleet confirm uses, and the outcome reports the shortfall:
"reached 9 of 12" rather than a success toast over a partial result.

Refresh says in its tooltip that it cannot ask the core for anything, because
the label promises a round trip the wire cannot make.

Known gaps, deliberate: the synthetic core swallows both new commands, so the
tab is inert under --fixture; and a reset still reaches only the connected cores
of a scope, which the confirm's count states but its text does not dwell on.
@guyverino
guyverino force-pushed the feat/core-status-problem-actions branch from 72225be to 9a0c486 Compare September 7, 2026 10:34
@guyverino
guyverino merged commit a57bb31 into main Sep 7, 2026
6 checks passed
@guyverino
guyverino deleted the feat/core-status-problem-actions branch September 7, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant