Skip to content

Commit 263183b

Browse files
committed
fix(webapp): queue watch recommendation must be a future condition; exit agent fullscreen on navigation
An already-late queue was recommended the age SLA — already true, so every watch one-shot with "that already happened". Late queue now recommends the drain (the recovery); a healthy queue recommends the age SLA. Navigating to another page drops the fullscreen takeover back to the side panel.
1 parent c9abfad commit 263183b

4 files changed

Lines changed: 53 additions & 33 deletions

File tree

apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SuggestedPrompt, WatchSpec } from "@internal/dashboard-agent-contracts";
2+
import { useLocation } from "@remix-run/react";
23
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
34
import {
45
ResizableHandle,
@@ -73,6 +74,21 @@ export function DashboardAgent({
7374
return !current;
7475
});
7576
}, []);
77+
78+
// Navigating to another page drops the takeover back to the side panel: the
79+
// user asked for a page (a navbar click, an agent navigation), so the page
80+
// must be what they see. Pathname only — filter and search-param changes stay
81+
// on the page and keep fullscreen.
82+
const { pathname } = useLocation();
83+
const previousPathname = useRef(pathname);
84+
useEffect(() => {
85+
if (previousPathname.current === pathname) return;
86+
previousPathname.current = pathname;
87+
setFullscreen((current) => {
88+
if (current) writeAgentFullscreen(false);
89+
return false;
90+
});
91+
}, [pathname]);
7692
// A request from `openWith`, handed to the panel. `seq` makes repeat requests
7793
// with the same text distinct, so the panel can tell them apart.
7894
// Bumped by contextual ⌘J while the panel is open; the panel starts a new

apps/webapp/app/components/dashboard-agent/watch-card.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,34 +51,34 @@ describe("the recommendations", () => {
5151

5252
it("recommends the condition §2.1 assigns to each object", () => {
5353
expect(runWatchRecommendation("run_abc123").kind).toBe("run_finished");
54-
expect(queueWatchRecommendation("email-sends").kind).toBe("backlog_drain");
54+
expect(queueWatchRecommendation("email-sends").kind).toBe("queue_oldest_age");
5555
expect(errorWatchRecommendation("error_a1b2c3d4").kind).toBe("error_recurrence");
5656
expect(healthWatchRecommendation("warn").kind).toBe("health_recovery");
5757
});
5858

59-
// §9.1: on a queue the page already calls late, "when it drains" is the wrong
60-
// promise — the useful watch is the wait itself.
61-
it("switches the queue recommendation to the age SLA once runs are already late", () => {
59+
// The recommendation must be a FUTURE condition: once runs are already late,
60+
// "if runs wait too long" is already true and would one-shot — the useful
61+
// promise flips to the recovery, "when it drains".
62+
it("switches the queue recommendation to the drain once runs are already late", () => {
6263
const late = queueWatchRecommendation("email-sends", {
6364
oldestWaitMs: OLDEST_WAIT_WARNING_MS,
6465
});
6566
expect(late).toMatchObject({
66-
kind: "queue_oldest_age",
67+
kind: "backlog_drain",
6768
queue: "email-sends",
68-
thresholdMinutes: OLDEST_WAIT_WARNING_MS / 60_000,
6969
});
7070
expect(watchSpecSchema.safeParse(late).success).toBe(true);
7171
});
7272

73-
it("stays on the drain when the queue is merely busy, or the signal is missing", () => {
73+
it("stays on the age SLA when the queue is merely busy, or the signal is missing", () => {
7474
expect(
7575
queueWatchRecommendation("email-sends", { oldestWaitMs: OLDEST_WAIT_WARNING_MS - 1 }).kind
76-
).toBe("backlog_drain");
76+
).toBe("queue_oldest_age");
7777
expect(queueWatchRecommendation("email-sends", { oldestWaitMs: null }).kind).toBe(
78-
"backlog_drain"
78+
"queue_oldest_age"
7979
);
80-
expect(queueWatchRecommendation("email-sends", {}).kind).toBe("backlog_drain");
81-
expect(queueWatchRecommendation("email-sends").kind).toBe("backlog_drain");
80+
expect(queueWatchRecommendation("email-sends", {}).kind).toBe("queue_oldest_age");
81+
expect(queueWatchRecommendation("email-sends").kind).toBe("queue_oldest_age");
8282
});
8383

8484
it("starts both follow-ups off — consent is never assumed", () => {
@@ -270,7 +270,7 @@ describe("the card's copy", () => {
270270
});
271271

272272
it("states the condition and the duration as §2.2 writes them", () => {
273-
const spec = queueWatchRecommendation("email-sends");
273+
const spec = queueWatchRecommendation("email-sends", { oldestWaitMs: OLDEST_WAIT_WARNING_MS });
274274
expect(watchConditionLabel(spec)).toBe("Until the queue drains");
275275
expect(watchDurationLabel(spec)).toBe("For 1 hour · checking every 5 min");
276276
});
@@ -314,7 +314,7 @@ describe("the card's copy", () => {
314314
describe("the persisted blocks (§2.2)", () => {
315315
it("states all four lifetime facts on a confirmation", () => {
316316
const body = watchConfirmationBlockBody({
317-
spec: queueWatchRecommendation("email-sends"),
317+
spec: queueWatchRecommendation("email-sends", { oldestWaitMs: OLDEST_WAIT_WARNING_MS }),
318318
watchId: "watch_1",
319319
});
320320
expect(body.outcome).toBe("watching");

apps/webapp/app/components/dashboard-agent/watch-recommendations.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
* | Object | Recommendation |
99
* |---|---|
1010
* | Run | when it finishes |
11-
* | Queue | when it drains — or, when runs are already waiting past the page's
12-
* warning threshold, when the wait crosses the SLA |
11+
* | Queue | if runs start waiting past the SLA — or, when the wait is already
12+
* past it, when the backlog drains |
1313
* | Error | if it happens again |
1414
* | Health (degraded) | when it recovers |
1515
*
@@ -39,30 +39,33 @@ export function runWatchRecommendation(runFriendlyId: string): WatchSpec {
3939
/**
4040
* A backlog is an aggregate, so the cadence starts at the 5-minute floor (§7.1).
4141
*
42-
* The recommendation is CONTEXTUAL: on a queue whose head-of-line run is already
43-
* waiting past the threshold the page tints warning at, "tell me when it drains"
44-
* is the wrong promise — the queue is not merely busy, runs are late, and the
45-
* useful watch is the wait itself. Everything else stays one tap deeper under
46-
* **Customize** (§2.1). The signal is threaded from the page, the same way
42+
* The recommendation is CONTEXTUAL, and it must be a FUTURE condition: a watch
43+
* whose condition is already true one-shots with "that already happened". On a
44+
* queue whose head-of-line run is already waiting past the threshold the page
45+
* tints warning at, the SLA breach is old news — the useful promise is the
46+
* recovery, "tell me when it drains". On a queue that is NOT late yet, the drain
47+
* may already be true (an empty queue) while "tell me if runs start waiting too
48+
* long" is the thing that hasn't happened. Everything else stays one tap deeper
49+
* under **Customize** (§2.1). The signal is threaded from the page, the same way
4750
* `degraded` is derived for the Investigate button; without it the recommendation
48-
* is the drain.
51+
* is the SLA.
4952
*/
5053
export function queueWatchRecommendation(
5154
queueName: string,
5255
context?: { oldestWaitMs?: number | null }
5356
): WatchSpec {
5457
const oldestWaitMs = context?.oldestWaitMs ?? null;
5558
if (oldestWaitMs !== null && oldestWaitMs >= OLDEST_WAIT_WARNING_MS) {
56-
return queueAgeWatchRecommendation(queueName);
59+
return {
60+
kind: "backlog_drain",
61+
queue: queueName,
62+
checkEveryMinutes: 5,
63+
maxHours: 1,
64+
note: `tell me when the ${queueName} queue drains`,
65+
};
5766
}
5867

59-
return {
60-
kind: "backlog_drain",
61-
queue: queueName,
62-
checkEveryMinutes: 5,
63-
maxHours: 1,
64-
note: `tell me when the ${queueName} queue drains`,
65-
};
68+
return queueAgeWatchRecommendation(queueName);
6669
}
6770

6871
/** "Runs wait longer than N minutes" — the SLA the queue page already calls late. */

apps/webapp/app/routes/storybook.agent-ui/route.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -622,8 +622,9 @@ const queueStalledDraft = withVariant(queueWatchDraft, "queue_stalled");
622622
const queueAgeDraft = withAgeMinutes(withVariant(queueWatchDraft, "queue_oldest_age"), 5);
623623

624624
// The contextual recommendation: on a queue whose oldest run is already waiting
625-
// past the page's warning threshold, the card OPENS on the age SLA instead of the
626-
// drain. Compact, because that is what the user sees first.
625+
// past the page's warning threshold the SLA is already breached, so the card
626+
// OPENS on the drain (the recovery) instead of the age SLA. Compact, because
627+
// that is what the user sees first.
627628
const lateQueueDraft = watchDraftFor(
628629
queueWatchRecommendation("email-sends", { oldestWaitMs: OLDEST_WAIT_WARNING_MS })
629630
);
@@ -1140,8 +1141,8 @@ const STATES: Record<string, React.ReactNode> = {
11401141
"watch-card-queue-age": (
11411142
<WatchCard draft={queueAgeDraft} onChange={noop} onSubmit={noop} defaultExpanded />
11421143
),
1143-
// Opened from a queue that is already late: the recommendation itself changed.
1144-
"watch-card-queue-age-recommended": (
1144+
// Opened from a queue that is already late: the recommendation flips to the drain.
1145+
"watch-card-queue-late-recommended": (
11451146
<WatchCard draft={lateQueueDraft} onChange={noop} onSubmit={noop} onCancel={noop} />
11461147
),
11471148

0 commit comments

Comments
 (0)