fix(desktop): stop the page watcher menu claiming nothing watches a page

Merged
HA
2mo ago
+686-555

Links

Summary

  • The page pane's watcher menu said "Nothing is watching this page" for pages that were being watched, contradicting the page header's own "An agent is watching" badge.
  • The menu now reads the org-wide watch state alongside the host's local list and reports three states instead of two, naming the watcher even when it isn't one this host runs.

Why / Context

The two surfaces answer different questions and the menu was printing one as the other.

The badge on the page (apps/web/.../PageWatchBadge.tsx) reads watchState() — the pages row's watchedByAgent + watchHeartbeatAt, fresh within 90s. That's org-wide: any host, any workspace.

The menu read usePageWatchers(workspaceId) → PageWatchManager.list(workspaceId), which is one host's in-memory map, filtered to one workspace:

for (const entry of this.entries.values()) {
    if (workspaceId && entry.workspaceId !== workspaceId) continue;

Only host services write the DB flag, so the DB is the union of every host's map and the menu was showing one slice of it. Reproducible three ways, all normal usage:

  1. An agent in workspace A watches the page; you open it from workspace B — WorkspacePagesMenu lists org-wide favorites and opens them in whatever workspace you're in.
  2. The watcher is on another machine.
  3. The watcher is in a cloud workspace.

In each case the truth is "yes, an agent is watching, and comments you leave reach it." The menu said the opposite.

How It Works

PageWatcherMenu now also runs cloudTrpc.page.get — the same query and the same 30s/5min polling the web badge uses — and falls back to it when the host has no local entry:

const watcher = pageId ? watchers.get(pageId) : undefined;   // host-local, authoritative
const elsewhere =
    !watcher && cloudWatch.data?.watch.watching === true      // org-wide fallback
        ? cloudWatch.data.watch
        : null;
StateTrigger chipMenu labelStop watching
Watched from this workspacedot + agent name"Comments go to this agent"yes, unchanged
Watched elsewhere (new)dot + agent name, or "An agent""Watched by an agent in another workspace"no — read-only
Not watchedBot icon"Nothing is watching this page"no

The assignable-terminal list is untouched: in the elsewhere state you can still pick a local agent, which takes over the watch.

Both mutations now invalidate the cloud query onSettled. Without it a local Stop watching would leave the cached flag true and relabel the page as watched-from-elsewhere for up to 30s — a new wrong answer in place of the old one.

Manual QA Checklist

Watched from this workspace (regression)

  • Publish a page from an agent terminal; the chip names the agent and the menu says "Comments go to this agent"
  • "Stop watching" clears it, and the menu drops to "Nothing is watching this page" immediately — not to the elsewhere label
  • The check mark still marks the terminal doing the watching

Watched elsewhere (the fix)

  • With a page watched by an agent in workspace A, open it from workspace B: menu reads "Watched by an agent in another workspace" and the chip names the agent
  • The page header badge and the menu now agree
  • No "Stop watching" row in this state
  • Assigning a local agent from this state takes over: label flips to "Comments go to this agent"

Not watched

  • A page with no watcher still reads "Nothing is watching this page"
  • Trigger shows the Bot icon, no status dot

General

  • No console errors in the renderer
  • Switch app language (e.g. ja, de) and confirm both new strings render translated

Testing

  • bun run --filter @superset/desktop typecheck
  • bun run lint:fix — clean
  • bun run check:i18n — all 17 locales compile under --strict
  • bun run sherif — no issues
  • bun test in packages/i18n — 157 pass
  • bun test PageWatcherMenu.test.tsx — 7 new tests, one per state, covering that the elsewhere state neither says "Nothing is watching this page" nor offers Stop watching

Not run: the full desktop suite, and bun run lint:check-node-imports (no such script in this repo any more). No Node imports were added — the change is one tRPC query in an existing renderer component.

Design Decisions

  • Read-only in the elsewhere state, rather than a working Stop watching. Clearing watchedByAgent from here would not touch the remote PageWatchManager's entry, and its next maybeHeartbeat (≤60s) would write the flag straight back — the watcher would appear to stop and then return. Making it stick needs a signal the remote manager can see, which is more than this fix is worth.
  • A separate page.get query rather than reading PageViewer's page.pull. pull presigns download URLs; polling it every 30s to track a heartbeat is wasteful. page.get is what the web badge already polls, so both surfaces now share a query shape.

Known Limitations

  • Assigning a local agent to a page watched elsewhere leaves both managers holding an entry, and their heartbeats fight over watchedByAgent until the remote one times out. Same root cause as the item below: a remote host has no way to hear that it lost the watch.
  • PageWatchManager.stop() clears its entries without calling clearWatch, so a host shutdown leaves the DB flag set until watchHeartbeatAt goes stale (≤90s). Deliberately out of scope.
  • The menu only renders for pages you own (PagePaneHeaderExtras gates on createdByUserId), so a page you don't own shows no watch information at all. Unchanged, also out of scope.

Follow-ups

  • Give the manager a way to notice a cleared flag, which would unlock a working Stop watching for remote watchers and settle the assign-takeover race.
  • Clear watch flags on graceful host shutdown.

Summary by cubic

Fixes the page watcher menu so it no longer falsely claims nothing watches a page that an agent on another host, workspace, or machine is watching, and now lists the actual watchers.

The menu previously read a single host's in-memory watch list and printed "Nothing is watching this page" for watchers that host couldn't see. It now gathers live watchers from every reachable host, names each session with its workspace, and links to the terminal. Watchers it can't enumerate are covered by the org-wide page flag and reported as such. Stop watching and the hand-off list are removed. A sandbox host is only queried while its workspace is on screen.

  • The watcher menu now shows nothing when no one watches a page, instead of "Nothing is watching this page".
  • Each watcher row carries the session title, names the workspace, and links to that agent's terminal.
  • Unnamed agents show as "An agent", and watchers on unreachable hosts show a status message.
  • Ending a watch now clears the badge within ~400ms; the flag is cleared before the host notifies, where it previously lingered up to 30s.
  • New tests cover local, remote, unnamed, and unwatched pages; new strings are translated in all 17 locales.

Summary by CodeRabbit

  • New Features
    • Page watcher indicators show the number of local agents watching and list their workspace and session details when available.
    • Selecting a local watcher opens its workspace. Watchers on unreachable hosts display a status message, with “An agent” shown when no name is available.
    • The watcher menu appears only when a page has watchers.
  • Localization
    • Updated watcher-related translations, including wording for comments directed to one or multiple agents.