Skip to content

feat(ui): show workspace health in the status bar - #1548

Merged
fluck-boss merged 3 commits into
risa-labs-inc:devfrom
Vidhaankhare16:feat/status-bar-workspace-health
Sep 23, 2026
Merged

fluck-boss merged 3 commits into
risa-labs-inc:devfrom
Vidhaankhare16:feat/status-bar-workspace-health

Conversation

@Vidhaankhare16

@Vidhaankhare16 Vidhaankhare16 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Refs #394

Problem

When the plugin sandbox stops a plugin after repeated failures, the only thing a person sees is one toast, and it disappears. After that the plugin is just missing. #394 describes this ("no way to find out"), and @surfthewave-commits asked there for the reason to be surfaced "without a click".

The data already exists. WorkspaceHealthReport (from #579/#710) reports a stopped plugin as plugin_stopped_after_failures. It also reports a browser engine that is missing, failed to start or is unresponsive, and MCP tools being withheld. But only boss status and boss doctor show it, so someone who doesn't open a terminal never sees it. Plugin Health & Recovery (#454) can fix plugin problems, but only if you already know to open it from the Help menu.

Change

  • Status bar: a health item appears next to the performance indicator only while there is a finding. It shows "1 issue" / "2 issues", amber for warnings and red if any finding is critical. When nothing is wrong it takes no space, so day-to-day use looks the same as before.
  • Dialog: clicking the item opens Workspace Health. It lists each finding with its severity, area, summary and the remedy boss doctor already prints. It also names any area that could not be checked or was only partly checked, so an empty list is never mistaken for a clean bill of health.
  • Fix buttons: a finding gets a button only when a screen exists that fixes it:
    • plugin_stopped_after_failures, plugin_needs_attention → Open Plugin Health & Recovery (feat(plugins): add health and recovery center #454)
    • browser_engine_not_installed → Open Browser Engine settings (download / reinstall)
    • Other findings (restart BOSS, repair a policy file) keep their remedy text but get no button, because a button that only repeats the advice would promise more than it does.
  • The dialog stays open after the item hides, so a problem fixed from inside it is seen to clear.

No new detection logic: the item reads the same WorkspaceHealthCollector the CLI uses, every 5 s, off the main thread. The collector never throws. In the bar itself, the only edit is one call in BossRightBottomBar, which adds no branches to a function already at detekt's complexity ceiling.

Keeping a 5 s read out of the log. Until now the report was read once per boss status / boss doctor query. Reading it every 5 s in every window means anything the read logs is repeated for as long as the condition lasts. So:

  • The read-only engine check (ChromiumAutoDownloader.isChromiumInstalledReadOnly, whose only caller is the health collector) now logs nothing. It used to log startup's decisions, such as "Chromium version mismatch" and "will re-download", which a status query never acts on. On macOS, an engine kept after its one repair attempt also logged a WARN on every call while being reported healthy. The check's signature is unchanged, so its detekt baseline entry still matches: the read-only path is recognised by the recordRepair it passes, a named no-op.
  • A health source that keeps throwing (a whole area in the collector, or one window's plugin source) is logged once, and again only after it has read cleanly (HealthSourceWarnings). Before, it logged a WARN on every read.

Design choices

  • Hidden when healthy, not a permanent green tick. The bar is shared with MCP, downloads and performance, and the plugin source briefly reads as unchecked at every launch. A permanent item, or one for "not fully checked", would flash on start-up and cost space for no information.
  • Fixes are keyed on HealthFinding.code, never on summary text, because codes are the stable identifiers.
  • Fixes reuse the application menu's own events (triggerShowPluginHealthCenter, triggerOpenSettings). The dialog closes first so two modals never stack.
  • The existing red MCP fault line is left as it is. It is persistent for the reason given at MCP kill-switch fails open on both read and write paths #85, and removing it is out of scope.

Tests

  • WorkspaceHealthIndicatorTest:
    • levels, labels, headline and coverage notes
    • a table covering every HealthCodes constant, with a reflection check so a code added later must be given a fix decision here
    • SettingsSection.valueOf(BROWSER_ENGINE_SETTINGS_SECTION), because triggerOpenSettings takes a string and a renamed section would otherwise fail silently
  • WorkspaceHealthStatusItemTest (Compose UI):
    • no item for a healthy report (after waiting for a real read, so the check is not vacuous)
    • the count for a degraded report
    • the dialog content lists finding, remedy and severity
    • exactly one fix button for one fixable plus one non-fixable finding, and the click reports the right fix
    • the coverage note for an unchecked area
    • each fix emits the right menu event for the window
    • the dialog stays open after the badge hides: open it from "1 issue", switch the next read to healthy, and it shows "No problems found" while the badge is gone
  • Log volume:
    • ChromiumAutoDownloaderTest: a read-only inspection of an engine kept after its repair writes no log line. Startup's own inspection still writes one, which shows the count is measuring something.
    • WorkspaceHealthSourcesTest: a window source failing on 3 reads is logged once, and failing again after a clean read is logged again.
    • WorkspaceHealthCollectorTest: an area failing on 3 reads is logged once.

Results (JDK 17, Windows 11): 59/59 across WorkspaceHealthStatusItemTest 8, WorkspaceHealthIndicatorTest 8, WorkspaceHealthCollectorTest 6, WorkspaceHealthSourcesTest 5, WorkspaceHealthReportTest 19 and ChromiumAutoDownloaderTest 13, 0 skipped, counted from the fresh test XML. detekt and ktlint clean on the same run.

The fix button's colours came out of that check rather than a review: rendering the dialog
offscreen to a PNG showed the button blank, because OutlinedButton's defaults come from
MaterialTheme, whose surface is light, so a BossTheme label on it was white on white.
It sets its own border and content colour now.

Known limitations

  • The report covers the whole workspace, but the fix opens in the window you clicked from. A plugin stopped in another window is listed, and its remedy says "in the affected window", which is the same wording boss doctor uses.
  • The 5 s refresh means a new problem can take up to 5 s to appear.
  • Logging a failing source once means a second, different failure of the same source is not logged until it has recovered in between. The status bar and boss doctor still show it.
  • Visual check done on Windows 11 only.

A plugin the sandbox stops after repeated failures is announced by one
toast that disappears, and is otherwise visible only through Help >
Plugin Health & Recovery or `boss doctor` in a terminal (risa-labs-inc#394).

Show the report `boss doctor` already builds in the status bar while it
has findings, and open a dialog listing each finding with its remedy and,
where a screen fixes it, a button to that screen: Plugin Health &
Recovery for plugin findings, Browser Engine settings for a missing
engine. Nothing is shown while the workspace is healthy.

The fix button sets its own border and colours rather than taking
OutlinedButton's defaults, which come from MaterialTheme: its surface is
light, so a BossTheme label on it rendered white on white and the button
read as blank. Found by rendering the dialog to a PNG offscreen, which is
the only way this class of defect shows up without running the app.

Refs risa-labs-inc#394
The health report used to be read by `boss status` and `boss doctor`, one
query at a time. The status item reads it every 5 s in every window, so
anything the read logs repeats for as long as the condition lasts:

- The read-only engine check logged startup's decisions ("version
  mismatch", "will re-download"), and on macOS a WARN on every call for an
  engine kept after its one repair - while reporting it healthy. A
  read-only inspection now logs nothing; the signature is unchanged.
- A health source that kept throwing logged a WARN per read. It is now
  logged once, and again only after it has read cleanly.

Also pin that the dialog outlives the badge once a problem is fixed from
inside it, and drop the literal backticks from the dialog's subtitle.
@fluck-boss

Copy link
Copy Markdown
Collaborator

@claude please review this exact head for merge into dev. Check that the workspace-health status bar item does no blocking IO or heavy work on the UI thread, polls or subscribes at a sane rate and stops when hidden or the window closes, handles missing or unhealthy workspaces without crashing, and has tests for the state mapping.

@claude

claude Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Claude finished @fluck-boss's task in 5m 9s —— View job


Review: workspace health in the status bar

  • Gather context (diff vs dev, read new + changed files)
  • UI thread: blocking IO / heavy work
  • Poll rate + lifecycle (hidden bar / window close)
  • Crash safety for missing or unhealthy workspaces
  • Tests for state mapping
  • Post review

Verdict on the four things you asked about

Question Answer
Blocking IO / heavy work on the UI thread No. withContext(Dispatchers.IO) wraps the whole read (WorkspaceHealthStatusItem.kt:72), and the plugin source is documented and written as an in-memory read (currentPluginHealth, no IO). Only the browser source touches the filesystem, and it does so on IO.
Sane rate Yes, 5 s, and the report is a data class, so produceState's default structuralEqualityPolicy means an unchanged report causes no recomposition at all. That is the right shape and worth keeping. One caveat below about it being per window.
Stops when hidden / window closes Yes. produceState cancels with the composition, and the bar is inside AnimatedVisibility(visible = shouldShowBottomBar(...)) (BossAppScaffold.kt:910), which removes the subtree. delay is cancellable, so while (true) is fine.
Handles missing / unhealthy workspaces Mostly. No window open → no registered plugin source → plugins unchecked, no badge; a throwing source is contained per-area and per-window. One residual hole, below.
Tests for state mapping Yes, and they are good. WorkspaceHealthIndicatorTest covers level, label, headline, coverage notes and the code→fix table, with a reflection check so a new HealthCodes constant must be given a decision, plus the SettingsSection.valueOf pin.

The code reads well and the docs-in-comments are up to this repo's standard. Below are the things I would want addressed or consciously accepted before merge; none of them is a "do not merge".


1. An uncaught throw in the producer takes more than the badge with it

WorkspaceHealthStatusItem.kt:70-75

while (true) {
    value = withContext(Dispatchers.IO) { readReport() }
    delay(refreshIntervalMs)
}

WorkspaceHealthCollector.collect() guards each source, but not itself: workspaceHealthReport(...), the plugins?.snapshots?.takeIf {} assembly and PluginSnapshotRead construction all run outside the per-source try. Anything that escapes there — or any Throwable that is neither Exception nor LinkageError — leaves the producer coroutine, which is a child of the Recomposer's effect job, not of a supervisorScope. The blast radius is not "the badge stops updating", it is the window's composition being cancelled, and there is no log line on the way out.

The odds are low (the collector's contract is "never throws", and workspaceHealthReport is pure), but this read went from once per boss status invocation to every 5 s in every window, which is exactly the change that turns a one-in-a-million path into a daily one. A try/catch around the assignment that logs (once, via HealthSourceWarnings, in the same spirit as the rest of the PR) and keeps looping costs three lines and makes the never-throws contract belt-and-braces rather than load-bearing.

Fix this →

2. The poll is per window, and the browser source is the one that costs something

DesktopWorkspaceHealthIndicator.kt:3 builds a fresh collector per read, and every window composes its own WorkspaceHealthStatusItem. So with four windows open, currentBrowserEngineHealth() runs four times every five seconds, forever, including for minimized windows — and that path is not free: ChromiumAutoDownloader.isChromiumInstalledReadOnly() stats the engine dir, stats executable.name, reads version.txt, and on macOS reads Info.plist for declaresBrowserTypes; then FluckEngine.resolveEngineDir walks the candidate list and chromiumVersionMismatch reads more.

It is small and it is off the UI thread, so this is not a defect. But the natural shape for app-wide state read by N windows is one app-level poller behind a shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1): it keeps the "stops when nothing is showing it" property you already have, collapses N reads to one, and has the side benefit that two windows can never disagree about the workspace's health. Worth doing now while there is one call site; the parameter seam for tests survives unchanged.

Related, and cheaper: in focus mode the bar auto-hides, so each hover-reveal restarts the loop with an immediate read. A shared feed with WhileSubscribed(stopTimeoutMillis = …) absorbs that too.

3. windowId == null silently swallows the fix

WorkspaceHealthStatusItem.kt:91

showDialog = false
windowId?.let { openHealthFix(fix, it) }

If LocalWindowId is unset the dialog closes and nothing whatsoever happens — the one outcome that reads as a broken button. The bar is always inside a window so this should not occur, but if it cannot occur it deserves a log line rather than a ?.let, and if it can, the button should not be offered. (Same reasoning this repo applies to MissingPluginOffer: a raised offer that quietly does nothing is worse than no offer.)

4. Known limitations worth adding to the PR body

  • The dialog is composed inside the bottom bar's subtree. Hiding the bar — focus mode's auto-hide, or the bar context menu — disposes an open Workspace Health dialog mid-read. The PR says "the dialog stays open after the item hides", which is true for the badge hiding but not for the bar hiding.
  • In focus mode the badge is invisible until the bar is revealed, so a stopped plugin is still unannounced for someone working with the bar hidden. Consistent with the MCP fault line beside it, so I would accept it — just say so.

5. readOnlyInspection as an identity sentinel

ChromiumAutoDownloader.kt:199-211. logger.takeUnless { recordRepair === readOnlyInspection } works, and the reason given for not adding a parameter (keeping the detekt baseline signature) is a real constraint in this repo. The fragility is that the previous read-only caller was written as recordRepair = {}, and the next person who wants a quiet inspection will write exactly that again and silently get logging back — the sentinel is invisible at the call site. The new ChromiumAutoDownloaderTest case pins both halves (quiet writes nothing, non-quiet writes one), which is the right mitigation; I would add one sentence to chromiumInstalledAt's own KDoc saying that a bare {} logs and readOnlyInspection does not, since that is where a future caller is reading.

6. The log-delta tests share a ring buffer

ChromiumAutoDownloaderTest, WorkspaceHealthCollectorTest and WorkspaceHealthSourcesTest all assert on BossLogger.getRecentLogs(limit = 1000) deltas. If the buffer rolls between the before and after reads, or another test in the same JVM logs concurrently, the delta shifts and the assertion flakes. WorkspaceHealthSourcesTest is already immune (it filters on a System.nanoTime()-unique windowId) and WorkspaceHealthCollectorTest filters on the area, which is narrow. ChromiumAutoDownloaderTest's count { it.component == "ChromiumAutoDownloader" } is the widest of the three. Low severity — noting it so it is recognised if it ever goes red in CI rather than being chased as a real regression.

7. Smaller notes

  • produceState keys. produceState(null, readReport, refreshIntervalMs) uses readReport as a remember key. The production default ::readWorkspaceHealth is stable (non-capturing callable reference), so the loop does not restart — but that is an implicit property of how Kotlin compiles the reference, and the tests that pass an inline lambda do restart the loop on each recomposition. Keying on refreshIntervalMs alone (with the reader read through rememberUpdatedState) makes the intent explicit and cannot regress.
  • No lifecycle test. Everything else the trigger comment asks about has a test; "the loop stops when the item leaves composition" does not. A read counter, rule.setContent {} replaced with empty content (or the item gated behind a mutableStateOf(false)), then asserting the counter stops growing, would pin the one property that is currently only true by construction.
  • HealthSourceWarnings.recovered(area.wireName) for PLUGINS is unreachable in practice, because WorkspaceHealthSources.pluginSnapshots() catches everything internally and never throws. Harmless defence, no change needed — just so nobody later "fixes" the per-window logging by removing the area-level guard.
  • HealthSourceWarnings keys are "plugins:<windowId>" versus the area's "plugins", so they cannot collide. Good.

What I checked and found correct

  • var showDialog by remember is declared before the ?: return, so the early return cannot skip a remember slot.
  • The dialog re-renders from the same current the poll updates, so the "fixed from inside, seen to clear" behaviour is real and the test for it waits on an actual read rather than on idle.
  • Fix mapping is keyed on HealthFinding.code, not summary text, and the reflection completeness check enforces that a new code gets a decision.
  • unregisterPlugins removes only while the source is still the registered one, and it clears the warning key.
  • OutlinedButton's explicit border/content colours: correct, and the reason in the comment matches what this repo has hit before with MaterialTheme defaults under BossTheme.

I did not run the build, detekt or the test suite here (no Gradle run in this environment); the PR reports 59/59 green with detekt and ktlint clean on JDK 17 / Windows 11, and nothing I read contradicts that.
· feat/status-bar-workspace-health

@fluck-boss

Copy link
Copy Markdown
Collaborator

Thanks @Vidhaankhare16 - this is a nice feature. The 5s read stays off the UI thread, it stops with composition, and the state-mapping tests are good.

Holding for one fix before merge:

  • Blocker: if the top-level health-report read throws, the exception can escape the effect and cancel the Recomposer's effect job, so one bad read breaks recomposition for the whole window instead of just the badge. Please contain it inside the effect (for example runCatching around each read, or a supervisor scope) so a failed read only blanks or degrades the badge. A test with a throwing reader would pin it.

Non-blocking, worth a look while you're in there:

  • Each open window polls separately and repeats the browser filesystem checks. A shared poller or short cache would avoid the duplicate work.
  • The Fix button closes silently when the window ID is missing. Showing a message or disabling it would be clearer.
  • The dialog can vanish when the bottom bar auto-hides.

The status item read the report in a produceState loop with nothing around
it. The collector contains each source, but the report is assembled outside
those guards, so anything escaping there left the effect and cancelled the
window's recomposer effect job instead of just the badge.

Each read now goes through readReportContained: Exception and LinkageError
are caught (the collector's pair), cancellation is rethrown, the loop keeps
the last report it showed, and the failure is logged once through
HealthSourceWarnings until a read succeeds again.
@Vidhaankhare16

Copy link
Copy Markdown
Contributor Author

Thanks @fluck-boss. The blocker is fixed in 2f63cf162.

What changed. WorkspaceHealthStatusItem's loop no longer lets a read throw out of the produceState effect. Each read now goes through readReportContained:

  • It catches Exception and LinkageError, the same two the collector catches for each source. CancellationException is rethrown, so the loop still stops when the item leaves composition.
  • A failed read returns null, and the loop keeps the report it last showed instead of blanking. That way a read failing once doesn't hide a stopped plugin the badge is already showing, and it doesn't close an open Workspace Health dialog.
  • The failure is logged once through HealthSourceWarnings (key report, separate from the per-area and per-window keys). It is logged again only if it fails again after a read succeeds.

Tests (WorkspaceHealthStatusItemTest):

  • a read that throws does not stop the badge or the loop: the reader throws, answers once, then throws on every later read. The badge still shows 1 issue after four reads. Against the old loop this test fails with the IllegalStateException escaping the effect, which is the failure you described.
  • a report that keeps failing is logged once until it reads again: three failures log once, and a failure after a clean read logs again. It filters on a unique error message, so the shared log ring buffer can't make it flaky.
  • cancellation is not contained.

WorkspaceHealthStatusItemTest 11/11 and the four ai.rever.boss.health test classes pass, 49/49 in total, on JDK 17 / Windows 11. ktlint and detekt are clean.

I haven't changed the non-blocking points in this push. The shared poller, the null-windowId Fix button and the dialog closing when the bar hides are good candidates for a follow-up PR.

@fluck-boss fluck-boss closed this Sep 23, 2026
@fluck-boss fluck-boss reopened this Sep 23, 2026
@fluck-boss
fluck-boss merged commit 62893d8 into risa-labs-inc:dev Sep 23, 2026
34 of 40 checks passed
fluck-boss pushed a commit that referenced this pull request Sep 25, 2026
#1688)

The health dialog is composed inside its status-bar item, so it exists only
while the bottom bar does. In focus mode the bar is shown only while the
pointer is over it, and opening the modal moves the pointer onto the
dialog: the bar hid and took the dialog with it.

Each window now has a BottomBarHolds, provided to the bar. The health item
holds the bar while its dialog is open, and shouldShowBottomBar counts a
hold the way it already counts Terminal setup's reopened dialog.

The Fix button also closed the dialog and opened nothing when the window
had no id to send the fix to. With no window id the dialog now leaves the
button out; each finding's "What to do" text still says what to do.

Follows up the #1548 review.
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.

2 participants