Skip to content

Commit 9316c81

Browse files
committed
feat(webapp): the raise-limit report action is self-serve — it links to the Concurrency page
Raising the env concurrency limit no longer routes through contact us: the footer's primary action navigates to the environment's Concurrency page (host-resolved path threaded to the pure card), with the docs button alongside.
1 parent 3106ce7 commit 9316c81

7 files changed

Lines changed: 57 additions & 9 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function DashboardAgentChat({
8080
prefill,
8181
promotedPrompt,
8282
watches,
83+
pagePaths,
8384
onCancelWatch,
8485
onTurnSettled,
8586
onActivityChange,
@@ -110,6 +111,8 @@ export function DashboardAgentChat({
110111
promotedPrompt?: SuggestedPrompt;
111112
// This chat's active watches, from the panel's history load.
112113
watches: WatchChip[];
114+
/** Host-resolved dashboard paths for settings-page footer actions. */
115+
pagePaths?: Record<string, string>;
113116
onCancelWatch: (watchId: string) => void;
114117
/** A watch was created — tell the panel to re-read the chips. */
115118
onTurnSettled: () => void;
@@ -370,6 +373,7 @@ export function DashboardAgentChat({
370373
onRetry={retry}
371374
onDismissError={clearError}
372375
onIntent={handleIntent}
376+
pagePaths={pagePaths}
373377
watches={watches}
374378
/>
375379
)}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export type DashboardAgentMessagesProps = {
4848
onIntent?: (intent: AgentIntent) => void;
4949
/** Host resolver for `trigger://` URIs a card cites. */
5050
resolveUri?: (uri: string) => ResolvedUri | null;
51+
/** Host-resolved dashboard paths for settings-page footer actions. */
52+
pagePaths?: Record<string, string>;
5153
/**
5254
* The chat's watches, when the host has them. A wake message names the watch
5355
* it came from, so this is what lets its banner say *what* was being watched
@@ -253,12 +255,14 @@ const DashboardAgentTurn = memo(function DashboardAgentTurn({
253255
message,
254256
onIntent,
255257
resolveUri,
258+
pagePaths,
256259
watches,
257260
investigationWinners,
258261
}: {
259262
message: UIMessage;
260263
onIntent?: (intent: AgentIntent) => void;
261264
resolveUri?: (uri: string) => ResolvedUri | null;
265+
pagePaths?: Record<string, string>;
262266
watches?: WakeWatch[];
263267
/** See {@link winningInvestigationOccurrences}. */
264268
investigationWinners?: Map<string, string>;
@@ -304,7 +308,12 @@ const DashboardAgentTurn = memo(function DashboardAgentTurn({
304308
if (blocks.length > 0) {
305309
body.push(
306310
<ChatCardSlot key={i}>
307-
<ViewBlocks blocks={blocks as never} onIntent={onIntent} resolveUri={resolveUri} />
311+
<ViewBlocks
312+
blocks={blocks as never}
313+
onIntent={onIntent}
314+
resolveUri={resolveUri}
315+
pagePaths={pagePaths}
316+
/>
308317
</ChatCardSlot>
309318
);
310319
}
@@ -366,6 +375,7 @@ export function DashboardAgentTurns({
366375
onDismissError,
367376
onIntent,
368377
resolveUri,
378+
pagePaths,
369379
watches,
370380
}: DashboardAgentMessagesProps) {
371381
// One status line at a time: a tool's own progress beats the generic activity.
@@ -387,6 +397,7 @@ export function DashboardAgentTurns({
387397
message={message}
388398
onIntent={onIntent}
389399
resolveUri={resolveUri}
400+
pagePaths={pagePaths}
390401
watches={watches}
391402
investigationWinners={investigationWinners}
392403
/>

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import type { SuggestedPrompt } from "@internal/dashboard-agent-contracts";
2626
import type { AgentPageContext } from "./page-context-types";
2727
import { agentPageLabel } from "./page-label";
28+
import { concurrencyPath } from "~/utils/pathBuilder";
2829

2930
// Restore the last open chat across panel re-opens and page reloads — but only
3031
// on the page it was last used on. A closed panel reopened on a DIFFERENT page
@@ -126,6 +127,13 @@ export function DashboardAgentPanel({
126127
// agent receives so the two can't disagree about where the user is.
127128
const currentPage = agentPageLabel(pageContext, location.pathname);
128129

130+
// Dashboard paths for report footer actions that live on a settings page —
131+
// built here because only the host knows the slugs (the card stays pure).
132+
const pagePaths = useMemo<Record<string, string>>(
133+
() => ({ raise_env_limit: concurrencyPath(organization, project, environment) }),
134+
[organization, project, environment]
135+
);
136+
129137
// The page context is a fresh object every render, so key the clientData memo
130138
// off its serialized form — otherwise every render would look like new
131139
// per-turn context to the transport.
@@ -514,6 +522,7 @@ export function DashboardAgentPanel({
514522
currentPage={currentPage}
515523
promotedPrompt={promotedPrompt}
516524
watches={chatWatches}
525+
pagePaths={pagePaths}
517526
onCancelWatch={cancelWatch}
518527
onTurnSettled={loadHistory}
519528
onActivityChange={handleActivityChange}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,16 +189,26 @@ function footerEntryNode({
189189
label,
190190
target,
191191
onIntent,
192+
pagePath,
192193
}: {
193194
code: string;
194195
label: string;
195196
target: LinkTarget;
196197
onIntent?: (intent: AgentIntent) => void;
198+
/** A host-resolved dashboard path for this action (settings pages). */
199+
pagePath?: string;
197200
}): ReactNode {
198201
const style = reportFooterStyle(code);
199202

200203
if (style === "note") return <ReportFooterNote>{label}</ReportFooterNote>;
201204

205+
// A settings-page action the host resolved for us wins over everything: the
206+
// user can self-serve it right there (e.g. raising the env concurrency limit
207+
// on the Concurrency page).
208+
if (style === "action" && pagePath) {
209+
return <ReportFooterActionLink href={pagePath}>{label}</ReportFooterActionLink>;
210+
}
211+
202212
// A docs entry is ALWAYS the docs button, whatever shape its link arrived in —
203213
// external URL, resolved resource, or nothing (then its canonical docs page).
204214
if (style === "docs") {
@@ -429,11 +439,19 @@ export function ReportView({
429439
onIntent,
430440
/** Host-supplied `trigger://` resolver. Without one, resource links stay intents. */
431441
resolveUri,
442+
/**
443+
* Host-supplied dashboard paths for footer actions that live on a settings
444+
* page rather than behind a URI (keyed by footer code, e.g. raise_env_limit
445+
* → the environment's Concurrency page). The component stays pure — only the
446+
* host knows the org/project/env slugs.
447+
*/
448+
pagePaths,
432449
}: {
433450
vm: ReportViewModelPayload;
434451
reportUri?: string;
435452
onIntent?: (intent: AgentIntent) => void;
436453
resolveUri?: (uri: string) => ResolvedUri | null;
454+
pagePaths?: Record<string, string>;
437455
}) {
438456
const messages = messagesFor(vm.title);
439457
const tokens = findingTokens(vm);
@@ -500,6 +518,7 @@ export function ReportView({
500518
}),
501519
target: classifyLink(linkByKey(entry.link), resolveUri),
502520
onIntent,
521+
pagePath: pagePaths?.[entry.code],
503522
}),
504523
}));
505524

apps/webapp/app/components/dashboard-agent/view-catalog.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ export function ViewBlocks({
3636
* for the server-side mapping.
3737
*/
3838
resolveUri,
39+
/** Host-resolved dashboard paths for settings-page footer actions. */
40+
pagePaths,
3941
}: {
4042
blocks: ViewBlock[];
4143
onIntent?: (intent: AgentIntent) => void;
4244
resolveUri?: (uri: string) => ResolvedUri | null;
45+
pagePaths?: Record<string, string>;
4346
}) {
4447
if (!Array.isArray(blocks)) return null;
4548
return (
@@ -65,6 +68,7 @@ export function ViewBlocks({
6568
reportUri={block.reportUri}
6669
onIntent={onIntent}
6770
resolveUri={resolveUri}
71+
pagePaths={pagePaths}
6872
/>
6973
);
7074
default:

apps/webapp/app/presenters/v3/reports/health/health.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,13 @@ function buildFooter(
123123
const dominant = dominantFinding(findings);
124124
if (!dominant?.recommendation) return [{ code: "nothing_to_do" }];
125125

126-
// Raising the env concurrency limit is a plan quota, not a self-serve
127-
// setting — the actionable step is contacting us, with the docs alongside.
126+
// Raising the env concurrency limit is self-serve now: the action button
127+
// takes the user to the environment's Concurrency page (the host resolves
128+
// the path), with the docs alongside.
128129
const footer: FooterEntry[] =
129130
dominant.recommendation.code === "raise_env_limit"
130131
? [
131-
{ code: "contact_us_raise_limit", link: "contact" },
132+
{ code: "raise_env_limit", link: dominant.recommendation.link },
132133
{ code: "concurrency_docs", link: dominant.recommendation.link },
133134
]
134135
: [{ code: dominant.recommendation.code, link: dominant.recommendation.link }];

apps/webapp/test/reportHealth.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ describe("health cause tree (Golden A — env limit saturation)", () => {
8181
expect(concurrency.annotation).toEqual({ code: "pinned_minutes", value: 40 });
8282
});
8383

84-
it("footer = contact us + docs + do-nothing (drains)", () => {
85-
// Raising the env limit is a plan quota, not self-serve — the footer offers
86-
// the contact action with the docs alongside.
84+
it("footer = raise the limit (self-serve) + docs + do-nothing (drains)", () => {
85+
// Raising the env limit is self-serve now — the action button leads to the
86+
// environment's Concurrency page, with the docs alongside.
8787
expect(vm.footer).toEqual([
88-
{ code: "contact_us_raise_limit", link: "contact" },
88+
{ code: "raise_env_limit", link: "concurrency" },
8989
{ code: "concurrency_docs", link: "concurrency" },
9090
{ code: "do_nothing_drains", value: 2.3 },
9191
]);
@@ -118,7 +118,7 @@ describe("health cause tree (Golden A — env limit saturation)", () => {
118118
119119
LIVENESS 🟢 fresh — telemetry current, updated 4s ago
120120
121-
Contact us to raise the limit
121+
Raise the env concurrency limit
122122
Read concurrency docs
123123
or do nothing — backlog drains in ~2.3 min once triggers ease"
124124
`);

0 commit comments

Comments
 (0)