Skip to content

Commit a3faea8

Browse files
committed
fix(webapp,dashboard-agent): wake quotes the right note and links its subject; 12px chat code blocks
Customize kept the recommendation's original note across condition/threshold changes, so the wake quoted a condition the user never watched — the note is now restated whenever the condition or its number changes. The wake prompt also hands the model a ready trigger:// markdown link to the watched object. Chat code examples drop to 12px.
1 parent 607734a commit a3faea8

4 files changed

Lines changed: 119 additions & 14 deletions

File tree

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,26 @@ describe("condition variants (§3)", () => {
131131
expect(variantsOf(watchDraftFor(healthWatchRecommendation("warn")))).toHaveLength(1);
132132
});
133133

134-
it("carries the subject, window and note across a swap", () => {
134+
it("carries the subject and window across a swap, and restates the note", () => {
135135
const draft = withWindow(runDraft(), 6);
136136
const failed = withVariant(draft, "run_failed");
137+
// The note is "why the user asked for it" and the wake quotes it — words
138+
// describing the OLD condition must not survive a condition change.
137139
expect(failed.spec).toMatchObject({
138140
kind: "run_failed",
139141
runId: "run_abc123",
140142
maxHours: 6,
141-
note: draft.spec.note,
143+
note: "tell me if run run_abc123 fails",
142144
});
143145
});
144146

147+
it("restates the note when the threshold number changes", () => {
148+
const above = withThreshold(withVariant(queueDraft(), "queue_depth_above"), 500);
149+
expect(above.spec.note).toBe("tell me if the email-sends queue grows above 500");
150+
const age = withAgeMinutes(withVariant(queueDraft(), "queue_oldest_age"), 90);
151+
expect(age.spec.note).toBe("tell me if runs in email-sends wait longer than 90 minutes");
152+
});
153+
145154
it("gives the threshold variant a usable default", () => {
146155
const above = withVariant(queueDraft(), "queue_depth_above");
147156
expect(above.spec).toMatchObject({ kind: "queue_depth_above", queue: "email-sends" });

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

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,48 @@ export function clampCadence(kind: WatchKind, minutes: number): number {
4545
return options.find((option) => option >= minutes) ?? options[options.length - 1]!;
4646
}
4747

48-
/** Swap the condition for its sibling variant (§3), carrying everything else. */
48+
/**
49+
* The note, restated from the spec. The note is "why the user asked for it" and
50+
* the wake narration quotes it — so once the user CHANGES the condition (or its
51+
* number), the original words no longer describe the watch and must be rewritten.
52+
* Edits that keep the condition (window, cadence) keep the user's own words.
53+
*/
54+
export function noteFor(spec: WatchSpec): string {
55+
switch (spec.kind) {
56+
case "run_start":
57+
return `tell me when run ${spec.runId} starts`;
58+
case "run_finished":
59+
return `tell me when run ${spec.runId} finishes`;
60+
case "run_failed":
61+
return `tell me if run ${spec.runId} fails`;
62+
case "backlog_drain":
63+
return `tell me when the ${spec.queue} queue drains`;
64+
case "queue_depth_above":
65+
return `tell me if the ${spec.queue} queue grows above ${spec.threshold}`;
66+
case "queue_depth_below":
67+
return `tell me when the ${spec.queue} queue is back below ${spec.threshold}`;
68+
case "queue_stalled":
69+
return `tell me if the ${spec.queue} queue stops moving`;
70+
case "queue_oldest_age":
71+
return `tell me if runs in ${spec.queue} wait longer than ${spec.thresholdMinutes} minutes`;
72+
case "error_recurrence":
73+
return `ping me if error ${spec.fingerprint} happens again`;
74+
case "health_recovery":
75+
return "tell me when health is back to normal";
76+
}
77+
}
78+
79+
/**
80+
* Swap the condition for its sibling variant (§3), carrying everything else —
81+
* except the note, which is restated to describe the NEW condition.
82+
*/
4983
export function withVariant(draft: WatchDraft, kind: WatchKind): WatchDraft {
84+
const next = variantSpec(draft, kind);
85+
if (next === draft.spec) return draft;
86+
return { ...draft, spec: { ...next, note: noteFor(next) } as WatchSpec };
87+
}
88+
89+
function variantSpec(draft: WatchDraft, kind: WatchKind): WatchSpec {
5090
const { spec } = draft;
5191
const common = {
5292
note: spec.note,
@@ -59,36 +99,36 @@ export function withVariant(draft: WatchDraft, kind: WatchKind): WatchDraft {
5999
case "run_failed":
60100
case "run_start": {
61101
const runId = "runId" in spec ? spec.runId : "";
62-
return { ...draft, spec: { ...common, kind, runId } as WatchSpec };
102+
return { ...common, kind, runId } as WatchSpec;
63103
}
64104
case "backlog_drain": {
65105
const queue = "queue" in spec ? spec.queue : "";
66-
return { ...draft, spec: { ...common, kind, queue } as WatchSpec };
106+
return { ...common, kind, queue } as WatchSpec;
67107
}
68108
case "queue_depth_above":
69109
case "queue_depth_below": {
70110
const queue = "queue" in spec ? spec.queue : "";
71111
// The number carries across the two threshold questions: someone who typed
72112
// 500 for "above" means the same 500 when they flip to "back below".
73113
const threshold = "threshold" in spec ? spec.threshold : WATCH_DEFAULT_QUEUE_THRESHOLD;
74-
return { ...draft, spec: { ...common, kind, queue, threshold } as WatchSpec };
114+
return { ...common, kind, queue, threshold } as WatchSpec;
75115
}
76116
case "queue_stalled": {
77117
const queue = "queue" in spec ? spec.queue : "";
78118
// K is not user-facing in this iteration (§3): the default is the product
79119
// decision, and the card never shows a field for it.
80120
const ticks = "ticks" in spec ? spec.ticks : WATCH_STALL_TICKS_DEFAULT;
81-
return { ...draft, spec: { ...common, kind, queue, ticks } as WatchSpec };
121+
return { ...common, kind, queue, ticks } as WatchSpec;
82122
}
83123
case "queue_oldest_age": {
84124
const queue = "queue" in spec ? spec.queue : "";
85125
const thresholdMinutes =
86126
"thresholdMinutes" in spec ? spec.thresholdMinutes : WATCH_DEFAULT_QUEUE_AGE_MINUTES;
87-
return { ...draft, spec: { ...common, kind, queue, thresholdMinutes } as WatchSpec };
127+
return { ...common, kind, queue, thresholdMinutes } as WatchSpec;
88128
}
89129
// The kinds with no second question keep the draft untouched.
90130
default:
91-
return draft;
131+
return draft.spec;
92132
}
93133
}
94134

@@ -125,7 +165,9 @@ export function withThreshold(draft: WatchDraft, threshold: number): WatchDraft
125165
if (draft.spec.kind !== "queue_depth_above" && draft.spec.kind !== "queue_depth_below") {
126166
return draft;
127167
}
128-
return { ...draft, spec: { ...draft.spec, threshold } };
168+
// The note quotes the number, so a new number restates the note.
169+
const spec = { ...draft.spec, threshold };
170+
return { ...draft, spec: { ...spec, note: noteFor(spec) } };
129171
}
130172

131173
/**
@@ -134,7 +176,9 @@ export function withThreshold(draft: WatchDraft, threshold: number): WatchDraft
134176
*/
135177
export function withAgeMinutes(draft: WatchDraft, thresholdMinutes: number): WatchDraft {
136178
if (draft.spec.kind !== "queue_oldest_age") return draft;
137-
return { ...draft, spec: { ...draft.spec, thresholdMinutes } };
179+
// The note quotes the number, so a new number restates the note.
180+
const spec = { ...draft.spec, thresholdMinutes };
181+
return { ...draft, spec: { ...spec, note: noteFor(spec) } };
138182
}
139183

140184
/**

apps/webapp/app/tailwind.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,8 @@
746746
@apply scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600;
747747
}
748748
& [data-code-block] pre code {
749-
@apply bg-transparent;
749+
/* 12px, chat-only: code examples read denser than the 14px prose around them. */
750+
@apply bg-transparent text-xs;
750751
}
751752
& [data-code-block] .line {
752753
@apply leading-relaxed;

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

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ async function generateAndSaveTitle(
351351

352352
import {
353353
agentPageContextSchema,
354+
formatTriggerUri,
354355
investigationStateSchema,
355356
isWatchKind,
356357
resolveWatchResult,
@@ -593,7 +594,48 @@ function investigationInstruction(action: WatchWakeAction): string {
593594
)}.`;
594595
}
595596

596-
function wakePrompt(action: WatchWakeAction): string {
597+
/**
598+
* The watched object as a `trigger://` markdown link the narration can embed —
599+
* the wake runs with no tools, so the link has to be handed to it ready-made.
600+
* Needs the tenancy from the wake's metadata; without it there is no link.
601+
*/
602+
function wakeSubjectLink(
603+
action: WatchWakeAction,
604+
tenancy: { projectRef?: string; environmentId?: string } | undefined
605+
): string | undefined {
606+
const projectRef = tenancy?.projectRef;
607+
const environmentId = tenancy?.environmentId;
608+
if (!projectRef || !environmentId) return undefined;
609+
610+
const spec = action.spec;
611+
const target =
612+
"queue" in spec && spec.queue
613+
? { kind: "queue" as const, projectRef, environmentId, name: spec.queue }
614+
: "runId" in spec && spec.runId
615+
? { kind: "run" as const, projectRef, environmentId, runId: spec.runId }
616+
: "fingerprint" in spec && spec.fingerprint
617+
? { kind: "error" as const, projectRef, environmentId, fingerprint: spec.fingerprint }
618+
: "report" in spec && spec.report
619+
? { kind: "report" as const, projectRef, environmentId, key: spec.report }
620+
: undefined;
621+
if (!target) return undefined;
622+
623+
const label =
624+
target.kind === "queue"
625+
? target.name
626+
: target.kind === "run"
627+
? target.runId
628+
: target.kind === "error"
629+
? "this error"
630+
: "the report";
631+
return `[${label}](${formatTriggerUri(target)})`;
632+
}
633+
634+
function wakePrompt(
635+
action: WatchWakeAction,
636+
tenancy?: { projectRef?: string; environmentId?: string }
637+
): string {
638+
const subjectLink = wakeSubjectLink(action, tenancy);
597639
return [
598640
WAKE_INSTRUCTION,
599641
`Resolution: ${wakeResolution(action)}${wakeOutcome(action)}.`,
@@ -603,6 +645,9 @@ function wakePrompt(action: WatchWakeAction): string {
603645
: undefined,
604646
action.note ? `Why the user asked for it: ${action.note}` : undefined,
605647
`Facts from the check:\n${JSON.stringify(action.facts, null, 2)}`,
648+
subjectLink
649+
? `When you point at the watched object, link it: ${subjectLink} — use this exact markdown link, not a bare name.`
650+
: undefined,
606651
wakeStartsInvestigation(action) ? investigationInstruction(action) : undefined,
607652
]
608653
.filter(Boolean)
@@ -723,7 +768,13 @@ async function narrateWatchWake(args: {
723768
// reads back the same cached prefix a normal turn would.
724769
messages: [
725770
...withCacheBreakpointOnLast(sanitizeReplayedToolInputs(args.messages)),
726-
{ role: "user" as const, content: wakePrompt(action) },
771+
{
772+
role: "user" as const,
773+
content: wakePrompt(action, {
774+
projectRef: args.clientData?.projectRef,
775+
environmentId: args.clientData?.environmentId,
776+
}),
777+
},
727778
],
728779
...resolved.toAISDKTelemetry(),
729780
});

0 commit comments

Comments
 (0)