Skip to content

Commit 41a2f4e

Browse files
committed
Merge remote-tracking branch 'origin/feat/dashboard-agent-flows-watch' into feat/agent-storybook-gallery
2 parents 510830d + 2cd73ec commit 41a2f4e

3 files changed

Lines changed: 25 additions & 25 deletions

File tree

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

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/**
2-
* The banner above a wake narration.
2+
* The banner above a wake narration: the label, the icon and the tone frame, and
3+
* nothing else. The narration under it states the headline, the user's note and the
4+
* next step, and each of those is said once per wake — so the banner marks the
5+
* message as a wake rather than restating it.
36
*
4-
* This component holds no kind-specific wording: tone, semantic icon and headline
5-
* come from contracts and `app/presenters/v3/dashboardAgent`. All it decides is which glyph a
7+
* This component holds no kind-specific wording: tone and semantic icon come from
8+
* contracts and `app/presenters/v3/dashboardAgent`. All it decides is which glyph a
69
* semantic icon draws and which frame a tone paints.
710
*
811
* A wake is identified by its message id, `wake:watch:{watchId}:{fired|expired}`.
@@ -23,11 +26,7 @@ import type {
2326
} from "@internal/dashboard-agent-contracts";
2427
import { cn } from "~/utils/cn";
2528
import { type AgentTone, TONE_ICON_COLOR } from "./agent-badges";
26-
import {
27-
presentResolvedWatch,
28-
watchSubline,
29-
WATCH_PRESENTATION_FALLBACK,
30-
} from "~/presenters/v3/dashboardAgent";
29+
import { presentResolvedWatch, WATCH_PRESENTATION_FALLBACK } from "~/presenters/v3/dashboardAgent";
3130

3231
const WAKE_ID_PREFIX = "wake:watch:";
3332

@@ -137,20 +136,15 @@ export function WakeBanner({
137136
const presentation = wakePresentation(outcome, watch);
138137
const tone = presentation.tone as AgentTone;
139138
const Icon = SEMANTIC_ICON[presentation.semanticIcon];
140-
const note = watchSubline(watch);
141139

142140
return (
143141
<div
144-
className={cn("flex items-start gap-2 rounded-r-md border-l-2 px-3 py-2", TONE_FRAME[tone])}
142+
className={cn("flex items-center gap-2 rounded-r-md border-l-2 px-3 py-2", TONE_FRAME[tone])}
145143
>
146-
<Icon className={cn("mt-0.5 size-4 shrink-0", TONE_ICON_COLOR[tone])} />
147-
<div className="min-w-0">
148-
<p className="text-xxs font-medium uppercase tracking-wider text-text-dimmed">
149-
{presentation.label}
150-
</p>
151-
<p className="text-sm font-medium text-text-bright">{presentation.headline}</p>
152-
{note ? <p className="truncate text-xs text-text-dimmed">{note}</p> : null}
153-
</div>
144+
<Icon className={cn("size-4 shrink-0", TONE_ICON_COLOR[tone])} />
145+
<p className="text-xxs font-medium uppercase tracking-wider text-text-dimmed">
146+
{presentation.label}
147+
</p>
154148
</div>
155149
);
156150
}

internal-packages/dashboard-agent/src/watch-narration.test.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@ describe("which model narrates a wake", () => {
3333
const plan = planWatchNarration(DRAINED);
3434
expect(plan.model).toBe("none");
3535
if (plan.model !== "none") throw new Error("unreachable");
36-
// The dashboard's own sentence, the user's reason, then what to do.
37-
expect(plan.text).toContain("task/send-receipt queue drained");
38-
expect(plan.text).toContain("You asked to be told when: tell me when the backlog drains");
39-
expect(plan.text).toContain("Nothing to do");
36+
// The dashboard's own sentence, the user's reason, then what to do — each once.
37+
// The banner above the wake carries the label and nothing else.
38+
expect(plan.text.split("\n\n")).toEqual([
39+
"task/send-receipt queue drained",
40+
"You asked to be told when: tell me when the backlog drains",
41+
"Nothing to do — I've stopped watching it.",
42+
]);
4043
});
4144

4245
it("needs no model when the answer is that the watched thing is gone", () => {
@@ -57,13 +60,15 @@ describe("which model narrates a wake", () => {
5760
expect(planWatchNarration({ ...DRAINED, startsInvestigation: true }).model).toBe("sonnet");
5861
});
5962

60-
it("links the watched object when the wake carries the tenancy for one", () => {
63+
it("links the watched object once, on the line that acts on it", () => {
6164
const { text } = deterministicWakeNarration({
6265
...DRAINED,
6366
subjectLink: "[task/send-receipt](trigger://queue/proj_abc/env_abc/task%2Fsend-receipt)",
6467
});
65-
expect(text).toContain("(trigger://queue/");
6668
expect(text).toContain("I've stopped watching [task/send-receipt]");
69+
// The headline already names the queue, so it is not followed by the link too.
70+
expect(text.split("\n\n")[0]).toBe("task/send-receipt queue drained");
71+
expect(text.match(/trigger:\/\//g)).toHaveLength(1);
6772
});
6873

6974
it("never says fired or expired", () => {

internal-packages/dashboard-agent/src/watch-narration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ export function deterministicWakeNarration(wake: NarratableWake): {
7272
});
7373
const lines = [
7474
// The headline is already a complete fact, and every surface states it this way.
75-
wake.subjectLink ? `${presentation.headline} (${wake.subjectLink})` : presentation.headline,
75+
// The subject is not repeated after it — the next step links it, once.
76+
presentation.headline,
7677
wake.note ? watchNoteLine(wake.note) : null,
7778
nextStep(presentation, wake.subjectLink),
7879
].filter((line): line is string => Boolean(line));

0 commit comments

Comments
 (0)