Stop telling users a busy agent is disconnected

The agent-poll indicator tracks whether a poll is parked, which is the right
signal for "can steering reach the agent right now" and is why the flag itself
is left alone. But it goes quiet for two different reasons, and both got the
same copy: "Agent disconnected - run live-poll.mjs to connect".

Under the one-shot foreground polling that live.md calls the primary contract,
no poll is parked while the agent works, so the second reason is every normal
generation. For its whole duration the bar told the user a healthy session was
broken and advised them to start a poll loop that was already running.

Pick the copy from the live state, which the browser already tracks: GENERATING
and SAVING mean the agent holds work it was handed, so say it is working. Every
other state with no parked poll keeps the original, actionable wording. The
aria-label carries the same distinction, since the tooltip is mouse-only.

The text is derived at read time rather than cached, because the live state moves
between the 5s status polls and a finished generation would otherwise keep
reading "Agent is working" until the next one landed. Deriving it also keeps the
read out of setLiveState, which runs long before agentPollingConnected's
declaration and would hit its temporal dead zone.

Prepared with AI assistance under maintainer direction.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-17 14:23:34 -07:00
co-authored by Claude
parent 917d3afcf2
commit 60c4fa25db
2 changed files with 55 additions and 6 deletions
+36 -6
View File
@@ -8561,6 +8561,13 @@ void main() {
const AGENT_STATUS_POLL_MS = 5000;
const AGENT_DISCONNECTED_MARK = 'oklch(62% 0 0 / 0.78)';
const AGENT_DISCONNECTED_TIP = 'Agent disconnected - run live-poll.mjs to connect';
// The indicator tracks whether a poll is parked, which is what decides if
// steering can reach the agent right now. That goes quiet two ways, and they
// need different copy: nobody is polling at all, or the agent took the work
// and is busy with it. Under one-shot foreground polling the second case is
// every normal generation, and telling the user to start a poll loop then is
// wrong advice about a healthy session.
const AGENT_BUSY_TIP = 'Agent is working - steering resumes when it finishes';
const GLOBAL_BAR_SECTION_GAP = 8;
const GLOBAL_BAR_INNER_GAP = 2;
const GLOBAL_BAR_INNER_PAD_LEFT = 2;
@@ -9581,15 +9588,36 @@ void main() {
</svg>`;
}
/**
* True while the browser is waiting on work it already handed to the agent.
* In these states a quiet poll indicator means "busy", not "absent".
*/
function agentHasWorkInFlight() {
return state === 'GENERATING' || state === 'SAVING';
}
/**
* Derived at read time, not cached: which of the two reasons applies depends on
* the live state, which moves between the 5s status polls. The truthiness is
* the same either way, so the indicator's visuals can stay driven by the
* cached value while the wording stays current.
*/
function agentStatusText() {
if (agentPollingConnected) return null;
return agentHasWorkInFlight() ? AGENT_BUSY_TIP : AGENT_DISCONNECTED_TIP;
}
function syncAgentPollingUi(connected) {
agentPollingConnected = !!connected;
if (!globalBarBrandEl) return;
const P = barPaletteForTheme(globalBarEl?.dataset.theme || detectPageTheme());
agentStatusMessage = connected ? null : AGENT_DISCONNECTED_TIP;
agentStatusMessage = agentStatusText();
globalBarBrandEl.dataset.agentConnected = connected ? 'true' : 'false';
globalBarBrandEl.setAttribute('aria-label', connected
? 'Impeccable live mode'
: 'Impeccable live mode - agent not polling');
// The tooltip is mouse-only, so carry the same distinction in the label or
// screen-reader users are left with the vaguer of the two readings.
globalBarBrandEl.setAttribute('aria-label', agentStatusMessage
? 'Impeccable live mode - ' + (agentHasWorkInFlight() ? 'agent is working' : 'agent not polling')
: 'Impeccable live mode');
globalBarBrandEl.removeAttribute('title');
globalBarBrandEl.style.cursor = agentStatusMessage ? 'help' : 'default';
const mark = globalBarBrandEl.querySelector('[data-brand-mark]');
@@ -9626,7 +9654,7 @@ void main() {
whiteSpace: 'normal',
});
agentPollTooltipEl.id = PREFIX + '-agent-poll-tooltip';
agentPollTooltipEl.textContent = agentStatusMessage || AGENT_DISCONNECTED_TIP;
agentPollTooltipEl.textContent = agentStatusText() || AGENT_DISCONNECTED_TIP;
uiAppend(agentPollTooltipEl);
return agentPollTooltipEl;
}
@@ -9634,7 +9662,9 @@ void main() {
function showAgentPollTooltip(anchor) {
if (!agentStatusMessage || !anchor) return;
const tip = ensureAgentPollTooltip();
tip.textContent = agentStatusMessage;
// Re-derive rather than reuse the cached copy: the live state may have moved
// since the last status poll set it.
tip.textContent = agentStatusText() || AGENT_DISCONNECTED_TIP;
tip.style.transition = 'none';
tip.style.display = 'block';
tip.style.opacity = '1';
+19
View File
@@ -469,6 +469,25 @@ describe('live-browser.js regression guards', () => {
/function syncAgentPollingUi\(/,
'global bar brand must reflect agent poll connectivity',
);
// The indicator goes quiet both when nobody is polling and when the agent
// holds leased work. Under one-shot foreground polling the second case is
// every normal generation, so a single "run live-poll.mjs to connect" tip
// told users to fix a healthy session.
assert.match(
SOURCE,
/function agentHasWorkInFlight\(\)\s*\{\s*return state === 'GENERATING' \|\| state === 'SAVING';/,
'agent poll copy must distinguish a busy agent from an absent one',
);
assert.match(
SOURCE,
/agentHasWorkInFlight\(\) \? AGENT_BUSY_TIP : AGENT_DISCONNECTED_TIP/,
'a busy agent must not be described as disconnected',
);
assert.match(
SOURCE,
/tip\.textContent = agentStatusText\(\)/,
'tooltip copy must be derived at display time, not read from a cache the 5s status poll last wrote',
);
assert.match(
SOURCE,
/case 'agent_polling':/,