Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
e22dc1a
fix(webapp): retry lazy code-renderer chunk load, fall back to plain …
kathiekiwi Aug 25, 2026
7ec06d9
fix(webapp): test the code-renderer fallback path and re-dispatch loa…
kathiekiwi Aug 25, 2026
deba2ca
feat(plugins): add organizationId to UAT claims for org-wide tokens
kathiekiwi Aug 26, 2026
83cdd09
feat(webapp): allow org-wide user-actor tokens to mint env JWTs acros…
kathiekiwi Aug 26, 2026
a9cbea4
test(webapp): route-level cases for org-wide user-actor JWT exchange
kathiekiwi Aug 26, 2026
5f38261
feat(run-engine): enumerate a queue's concurrency slot holders
kathiekiwi Aug 26, 2026
76673bc
feat(webapp): report queue slot holders on queue retrieve
kathiekiwi Aug 26, 2026
efb6f98
refactor(webapp): drop holderResolution from queue slot holders
kathiekiwi Aug 26, 2026
b62a291
feat(dashboard-agent): surface queue slot holders in get_queue
kathiekiwi Aug 25, 2026
7e30696
fix(dashboard-agent): ground partial/unresolved slot-holder states
kathiekiwi Aug 25, 2026
6f4301a
feat(dashboard-agent): pass through slotHolderFacts, ground phase/facts
kathiekiwi Aug 26, 2026
14a2f68
docs(dashboard-agent): match get_queue tool description to slotHolder…
kathiekiwi Aug 26, 2026
b51892c
fix(dashboard-agent): drop holderResolution, ground incomplete slot l…
kathiekiwi Aug 26, 2026
d758f6d
fix(dashboard-agent): stamp dirty-deployment flag on repo tool outputs
kathiekiwi Aug 26, 2026
63bcba8
fix(dashboard-agent): make the dirty-sha stamp sticky-true
kathiekiwi Aug 26, 2026
9a8c973
feat(webapp): make the agent token's organization its authorization b…
kathiekiwi Aug 26, 2026
0668fc4
fix(webapp): honour an org-wide agent token when unsubscribing a watc…
kathiekiwi Aug 26, 2026
8a78fbd
fix(dashboard-agent): ground run wait, span ids, and error recurrence
kathiekiwi Aug 26, 2026
0da46d3
fix(dashboard-agent): cap run wait at finishedAt for never-started te…
kathiekiwi Aug 26, 2026
0f8bd79
feat(dashboard-agent): surface env-scope concurrency and override bre…
kathiekiwi Aug 26, 2026
c34af0e
fix(dashboard-agent): ground envConcurrency in the burst-factor gate,…
kathiekiwi Aug 26, 2026
f904497
feat(dashboard-agent): give concurrency_saturation signal identity
kathiekiwi Aug 26, 2026
c229222
test(dashboard-agent): fix stale span-evidence test, add rejection case
kathiekiwi Aug 26, 2026
aef849f
fix(dashboard-agent): trim system prompt to fit char budgets
kathiekiwi Aug 26, 2026
e8af577
fix(dashboard-agent): restore holder-vs-facts mismatch distinction in…
kathiekiwi Aug 26, 2026
1e021c4
chore(scripts): add dashboard-agent UAT seed script
kathiekiwi Aug 26, 2026
44e070e
fix(webapp): hide hypotheses count on investigation toggle when zero
kathiekiwi Aug 26, 2026
790e115
fix(webapp): show Investigate button for failed runs without a struct…
kathiekiwi Aug 26, 2026
10c3d2b
docs(webapp): add server-changes note for grounded queue answers
kathiekiwi Aug 26, 2026
c2952a7
fix(webapp): use import type instead of import() type annotations in …
kathiekiwi Aug 26, 2026
55f0211
chore(scripts): add --user flag to dashboard-agent UAT seed script
kathiekiwi Aug 26, 2026
64d84db
chore(scripts): document type=custom queue lookup contract in UAT see…
kathiekiwi Aug 26, 2026
cb363e7
fix(webapp): bound the dashboard agent's live-turn waits
kathiekiwi Aug 26, 2026
b5d8144
fix(webapp): address review — gate tool deadline, re-arm on retry
kathiekiwi Aug 26, 2026
9d65091
fix(webapp): re-arm first-event deadline across a same-status retry
kathiekiwi Aug 26, 2026
f64c674
fix(webapp): dismiss shouldn't re-arm the first-event deadline
kathiekiwi Aug 26, 2026
9c2082b
feat(dashboard-agent): let data lookups target another project/enviro…
kathiekiwi Aug 26, 2026
62c1940
fix(dashboard-agent): sharpen cross-project override wording and erro…
kathiekiwi Aug 26, 2026
49601a7
chore(webapp): trim comments to the essential why across the agent PR
kathiekiwi Aug 26, 2026
aa84c4b
fix(dashboard-agent): sweep sibling envs before reporting not-found
kathiekiwi Aug 26, 2026
be828ec
fix(dashboard-agent): scope not-found sweep claims to this turn
kathiekiwi Aug 26, 2026
5c3c688
fix(dashboard-agent): ban restating an investigation card in the clos…
kathiekiwi Aug 26, 2026
42a89b4
fix(dashboard-agent): scope the no-restatement ban to the closing lin…
kathiekiwi Aug 26, 2026
34d0165
fix(dashboard-agent): trigger investigations by question type, not an…
kathiekiwi Aug 26, 2026
ebbf6b7
fix(dashboard-agent): resolve health-question and concluded-definitio…
kathiekiwi Aug 26, 2026
cc84011
fix(dashboard-agent): make the first not-found sweep mandatory, ban p…
kathiekiwi Aug 27, 2026
559fc0b
fix(dashboard-agent): restore the environment-switcher ban dropped by…
kathiekiwi Aug 27, 2026
c4d0e06
fix(dashboard-agent): move the mandatory-sweep imperative into tool d…
kathiekiwi Aug 27, 2026
ca97486
fix(dashboard-agent): scope correlate_version's dev-run exception to …
kathiekiwi Aug 27, 2026
5c65e5f
chore(scripts): add headless dashboard-agent asker
kathiekiwi Aug 27, 2026
88d0977
fix(dashboard-agent): scope list_projects to the conversation's own o…
kathiekiwi Aug 27, 2026
8f6f008
fix(dashboard-agent): error instead of an empty list_projects when or…
kathiekiwi Aug 27, 2026
235cfc3
chore(scripts): register a consumer task for S3, fix S10 CH insert fi…
kathiekiwi Aug 27, 2026
4a4ba67
feat(sdk): allow a ttl on session-triggered runs
kathiekiwi Aug 27, 2026
bd65fa7
fix(webapp): expire dashboard agent turn runs that are never dequeued
kathiekiwi Aug 27, 2026
2a258e7
test(webapp): assert the session ttl reaches the trigger options
kathiekiwi Aug 27, 2026
4ed9a87
fix(dashboard-agent): an inaccessible scope never stops the mandatory…
kathiekiwi Aug 27, 2026
b9285db
fix(dashboard-agent): sweep siblings directly, without list_environments
kathiekiwi Aug 27, 2026
e0d1943
fix(webapp): name the saturated queue in the suggested prompt
kathiekiwi Aug 27, 2026
7ac6b06
fix(webapp): merge a settled user turn by text, not just id
kathiekiwi Aug 27, 2026
61c1d14
fix(dashboard-agent): replace the no-restatement ban with a prescribe…
kathiekiwi Aug 27, 2026
7b5b92e
fix(dashboard-agent): soften the leaked/stale ban to its original intent
kathiekiwi Aug 27, 2026
e3c2692
feat(dashboard-agent): sweep other envs per project, cross-project wa…
kathiekiwi Aug 27, 2026
59528d9
feat(webapp): complete the cross-project watch confirm loop
kathiekiwi Aug 27, 2026
fede1a0
fix: adjudicated PR #4796 review fixes
kathiekiwi Aug 27, 2026
2381a8a
fix(dashboard-agent): reconcile leaked/stale and closing-message rules
kathiekiwi Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/chat-session-run-ttl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@trigger.dev/core": patch
"@trigger.dev/sdk": patch
---

Chat server sessions can now set a `ttl` on the runs they trigger, so a run that is never picked up expires instead of waiting indefinitely.
6 changes: 6 additions & 0 deletions .server-changes/agent-chat-code-renderer-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---

Fix the assistant chat showing a full-screen error when code highlighting fails to load. It now retries automatically and falls back to plain text so the conversation stays usable.
6 changes: 6 additions & 0 deletions .server-changes/agent-grounded-queue-answers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

The AI assistant now gives grounded answers about queues: it can name the exact runs occupying concurrency slots and which limit is blocking, report accurate queue wait times, and help across all projects in your organization.
6 changes: 6 additions & 0 deletions .server-changes/dashboard-agent-bounded-wait-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---

The dashboard agent chat no longer waits forever on a stuck response or tool call — it now shows a clear error with a "Try again" option instead of hanging silently.
59 changes: 57 additions & 2 deletions apps/webapp/app/components/code/StreamdownRenderer.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { restrictModelUrls, StreamdownRenderer } from "./StreamdownRenderer";
import { describe, expect, it, vi } from "vitest";
import {
loadStreamdownRenderer,
restrictModelUrls,
retryImport,
StreamdownRenderer,
} from "./StreamdownRenderer";

// streamdown calls urlTransform(url, key, node) to compute each url attribute; a
// returned undefined removes the attribute, so no request is ever issued.
Expand Down Expand Up @@ -88,3 +93,53 @@ describe("StreamdownRenderer (rendered markdown)", () => {
expect(html).toContain('src="/local/pic.png"');
});
});

describe("retryImport", () => {
it("resolves on first success", async () => {
const importer = vi.fn().mockResolvedValue("ok");
await expect(retryImport(importer, [0, 0])).resolves.toBe("ok");
expect(importer).toHaveBeenCalledTimes(1);
});

it("retries after failures then succeeds", async () => {
const importer = vi
.fn()
.mockRejectedValueOnce(new Error("fail1"))
.mockRejectedValueOnce(new Error("fail2"))
.mockResolvedValue("ok");
await expect(retryImport(importer, [0, 0])).resolves.toBe("ok");
expect(importer).toHaveBeenCalledTimes(3);
});

it("throws after exhausting retries", async () => {
const importer = vi.fn().mockRejectedValue(new Error("always fails"));
await expect(retryImport(importer, [0, 0])).rejects.toThrow("always fails");
expect(importer).toHaveBeenCalledTimes(3);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});

describe("loadStreamdownRenderer", () => {
it("resolves to a plain-text fallback when the chunk load keeps failing", async () => {
// The fallback path re-raises as an unhandled rejection (for StaleAssetRecovery); swap
// in our own listener so it's asserted on, not reported as a test-runner failure.
const priorListeners = process.listeners("unhandledRejection");
process.removeAllListeners("unhandledRejection");
const caught = new Promise<Error>((resolve) => {
process.once("unhandledRejection", (err) => resolve(err as Error));
});

try {
const mod = await loadStreamdownRenderer(() => Promise.reject(new Error("boom")), [0, 0]);
const html = renderToStaticMarkup(createElement(mod.default, null, "hello **world**"));
expect(html).toContain("hello");

const dispatched = await caught;
expect(dispatched.message).toMatch(/boom/);
} finally {
process.removeAllListeners("unhandledRejection");
for (const listener of priorListeners) {
process.on("unhandledRejection", listener as NodeJS.UnhandledRejectionListener);
}
}
Comment thread
kathiekiwi marked this conversation as resolved.
});
});
53 changes: 47 additions & 6 deletions apps/webapp/app/components/code/StreamdownRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { lazy } from "react";
import type { CodeHighlighterPlugin, UrlTransform } from "streamdown";
import type * as StreamdownModule from "streamdown";
import type * as StreamdownCodeModule from "@streamdown/code";
import type * as ShikiThemeModule from "./shikiTheme";

const SAFE_LINK_SCHEMES = new Set(["http:", "https:", "mailto:"]);

Expand Down Expand Up @@ -35,9 +38,41 @@ export const restrictModelUrls: UrlTransform = (url, key, node) => {
return SAFE_LINK_SCHEMES.has(`${schemeMatch[1].toLowerCase()}:`) ? url : undefined;
};

export const StreamdownRenderer = lazy(() =>
Promise.all([import("streamdown"), import("@streamdown/code"), import("./shikiTheme")]).then(
([{ Streamdown }, { createCodePlugin }, { triggerDarkTheme }]) => {
const RETRY_DELAYS_MS = [250, 1000];

/** Retries a lazy import a few times before giving up, so a flaky chunk load doesn't crash the chat. */
export async function retryImport<T>(
importer: () => Promise<T>,
delaysMs: number[] = RETRY_DELAYS_MS
): Promise<T> {
for (let attempt = 0; ; attempt++) {
try {
return await importer();
} catch (error) {
if (attempt >= delaysMs.length) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, delaysMs[attempt]));
}
}
}

const PlainTextFallback = ({ children }: { children: string }) => (
<pre className="whitespace-pre-wrap break-words font-sans text-sm">{children}</pre>
);

type StreamdownRendererModule = {
default: (props: { children: string; isAnimating?: boolean }) => JSX.Element;
};

export function loadStreamdownRenderer(
load: () => Promise<
[typeof StreamdownModule, typeof StreamdownCodeModule, typeof ShikiThemeModule]
> = () => Promise.all([import("streamdown"), import("@streamdown/code"), import("./shikiTheme")]),
delaysMs?: number[]
): Promise<StreamdownRendererModule> {
return retryImport(load, delaysMs)
.then(([{ Streamdown }, { createCodePlugin }, { triggerDarkTheme }]) => {
// Type assertion needed: @streamdown/code and streamdown resolve different shiki
// versions under pnpm, causing structurally-identical CodeHighlighterPlugin types
// to be considered incompatible (different BundledLanguage string unions).
Expand All @@ -64,6 +99,12 @@ export const StreamdownRenderer = lazy(() =>
</Streamdown>
),
};
}
)
);
})
.catch((error) => {
// Re-raise as an unhandled rejection so StaleAssetRecovery can reload on deploy skew.
queueMicrotask(() => void Promise.reject(error));
return { default: PlainTextFallback };
});
}

export const StreamdownRenderer = lazy(() => loadStreamdownRenderer());
111 changes: 103 additions & 8 deletions apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx

@devin-ai-integration devin-ai-integration Bot Aug 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Bounded-wait retry can resend during a live turn

effectiveError in DashboardAgentChat.tsx surfaces a deadline error while the SDK status is still submitted/streaming and error is undefined. The retry callout then calls retry(), which can sendMessage/regenerate while the previous turn's stream is still open server-side. This is the intended bounded-wait behavior, but overlapping sends depend on how useChat handles a resend mid-turn; worth confirming it does not duplicate turns.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intended bounded-wait behavior: after the first-event deadline fires, the prior turn is presumed dead and retry is an explicit user action — resending is the recovery path, not a double-send during a live turn.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@internal/dashboard-agent-contracts";
import { useLocation, useNavigate } from "@remix-run/react";
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useToast } from "~/components/primitives/Toast";
import { AgentQuotaNotice, AgentUpgradeBlock } from "./AgentUpgradeGate";
import { DashboardAgentComposer } from "./DashboardAgentComposer";
Expand All @@ -27,15 +27,25 @@ import { createTranscriptOrder, orderTranscript } from "./message-order";
import { navigateDestination } from "./navigate-target";
import { pendingNavigateIntents, pendingWatchIntents } from "./pending-intents";
import type { AgentPageContext } from "./page-context-types";
import { inFlightToolName } from "./progress-line";
import { retryAction } from "./retry-action";
import {
fetchChatTranscript,
pollSettledTranscript,
transcriptLooksUnfinished,
} from "./settled-transcript";
import { toolPendingLabel } from "./tool-labels";
import { takeNavigateIntent } from "./turn-navigation";
import { sendRequestOutcome } from "./send-request";
import { teardownCancelsTurn, unmountTeardown } from "./turn-teardown";
import {
activeToolPendingKey,
createKeyedDeadline,
FIRST_EVENT_DEADLINE_MS,
TOOL_PENDING_DEADLINE_MS,
turnDeadlineErrorMessage,
type TurnDeadlineError,
} from "./turn-deadlines";
import { useAgentMessageQuota } from "./useAgentMessageQuota";
import { useTriggerUriResolver } from "./useTriggerUriResolver";
import { WatchChips, type WatchChip } from "./WatchChips";
Expand Down Expand Up @@ -80,6 +90,8 @@ export function DashboardAgentChat({
onTurnSettled,
onActivityChange,
onQuotaChange,
firstEventDeadlineMs = FIRST_EVENT_DEADLINE_MS,
toolPendingDeadlineMs = TOOL_PENDING_DEADLINE_MS,
}: {
chatId: string;
initialMessages: UIMessage[];
Expand All @@ -102,13 +114,18 @@ export function DashboardAgentChat({
pagePaths?: Record<string, string>;
watchCard?: React.ReactNode;
appendedMessages?: { messages: UIMessage[]; seq: number };
/** Nothing is persisted until the user submits the card. */
onWatchIntent?: (spec: WatchSpec) => void;
/** Nothing is persisted until the user submits the card. `target` is set only when
* the watch targets another project/environment than this chat's own. */
onWatchIntent?: (spec: WatchSpec, target?: { environmentId: string }) => void;
onCancelWatch: (watchId: string) => void;
onTurnSettled: () => void;
onActivityChange?: (chatId: string, activity: TurnActivity | null) => void;
/** The poll lives here, so this is where the panel learns the cap has lifted. */
onQuotaChange?: (quota: MessageQuota) => void;
/** How long to wait for the first stream event before showing a bounded-wait error. */
firstEventDeadlineMs?: number;
/** How long a single pending tool call can run before showing a bounded-wait error. */
toolPendingDeadlineMs?: number;
}) {
const [input, setInput] = useState("");
// Set when the server refuses a send over the cap, so the block shows at once rather than
Expand Down Expand Up @@ -213,6 +230,53 @@ export function DashboardAgentChat({

const messages = orderTranscript(rawMessages, orderRef.current);

// Independent of the SDK's own `error`: both drive the live-error callout, but a
// deadline firing never touches the server turn or `status`.
const [deadlineError, setDeadlineError] = useState<TurnDeadlineError | null>(null);
// Bumped in `retry` to force the first-event effect to re-run when a resend reuses the
// same `status: "submitted"`. `dismissError` never bumps it.
const [attempt, setAttempt] = useState(0);
const firstEventDeadline = useRef(
createKeyedDeadline<"submitted">({
deadlineMs: firstEventDeadlineMs,
onTimeout: () => setDeadlineError({ kind: "first-event" }),
onClear: () =>
setDeadlineError((current) => (current?.kind === "first-event" ? null : current)),
})
).current;
const toolPendingDeadline = useRef(
createKeyedDeadline<string>({
deadlineMs: toolPendingDeadlineMs,
onTimeout: (tool) => setDeadlineError({ kind: "tool-pending", tool }),
onClear: () =>
setDeadlineError((current) => (current?.kind === "tool-pending" ? null : current)),
})
).current;
useEffect(() => {
firstEventDeadline.sync(status === "submitted" ? "submitted" : null);
// `attempt` forces a re-sync when `status` is unchanged across a retry (see its
// declaration above).
}, [status, firstEventDeadline, attempt]);
useEffect(() => {
toolPendingDeadline.sync(activeToolPendingKey(status, inFlightToolName(messages)));
}, [messages, status, toolPendingDeadline]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
useEffect(
() => () => {
firstEventDeadline.dispose();
toolPendingDeadline.dispose();
},
[firstEventDeadline, toolPendingDeadline]
);
// The SDK's own error wins when both are present — it's the more specific failure.
const effectiveError = useMemo(
() =>
error ??
(deadlineError
? new Error(turnDeadlineErrorMessage(deadlineError, toolPendingLabel))
: undefined),
[error, deadlineError]
);

// Read here, not in the panel, so it re-reads as each turn settles.
const quota = useAgentMessageQuota({ actionPath, chatId, status });
useEffect(() => {
Expand Down Expand Up @@ -299,13 +363,36 @@ export function DashboardAgentChat({
);
if (!action) return;
clearError();
setDeadlineError(null);
// Reset the deadlines' own key, not just the displayed error: a dangling tool part
// that already fired once would otherwise never re-arm (same key, no change to sync).
firstEventDeadline.sync(null);
toolPendingDeadline.sync(null);
// Forces the first-event effect to re-sync even when `status` stays "submitted" across
// the retry (a resend re-enters "submitted", the same value the failed turn left it in).
setAttempt((current) => current + 1);
turnStartedPathRef.current = renderedPathRef.current;
if (action.kind === "regenerate") {
void regenerate();
return;
}
void sendMessage({ text: action.text, messageId: action.messageId });
}, [messages, sendMessage, regenerate, clearError, atMessageCap]);
}, [
messages,
sendMessage,
regenerate,
clearError,
atMessageCap,
firstEventDeadline,
toolPendingDeadline,
]);

const dismissError = useCallback(() => {
clearError();
setDeadlineError(null);
firstEventDeadline.sync(null);
toolPendingDeadline.sync(null);
}, [clearError, firstEventDeadline, toolPendingDeadline]);
Comment on lines +390 to +395

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Dismissed bounded-wait error never re-arms during the same turn

dismissError clears the error and calls sync(null) on both deadlines but never bumps attempt, unlike retry. With status and messages unchanged, the deadline effects do not re-run, so a still-running turn stays unbounded after one dismiss. Confirm this is the intended dismiss semantics.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Owner decision — dismiss means 'stop telling me', so it deliberately does not bump the attempt counter (unlike retry, which re-arms). Intended asymmetry.


const resolveUri = useTriggerUriResolver(actionPath);

Expand Down Expand Up @@ -345,7 +432,10 @@ export function DashboardAgentChat({
submit(intent.prompt);
return;
case "watch":
onWatchIntent?.(intent.spec);
onWatchIntent?.(
intent.spec,
intent.target ? { environmentId: intent.target.environmentId } : undefined
);
return;
case "navigate":
void goTo(intent);
Expand Down Expand Up @@ -383,7 +473,12 @@ export function DashboardAgentChat({
useEffect(() => {
const pending = pendingWatchIntents(messages, watchProposedRef.current!);
const proposed = pending.at(-1);
if (proposed) onWatchIntent?.(proposed.spec);
if (proposed) {
onWatchIntent?.(
proposed.spec,
proposed.target ? { environmentId: proposed.target.environmentId } : undefined
);
}
}, [messages, onWatchIntent]);

const stop = useCallback(() => {
Expand Down Expand Up @@ -450,10 +545,10 @@ export function DashboardAgentChat({
<DashboardAgentMessages
messages={messages}
activity={activity}
error={error}
error={effectiveError}
onRetry={retry}
retryDisabledReason={atMessageCap ? MESSAGE_QUOTA_REACHED_REASON : undefined}
onDismissError={clearError}
onDismissError={dismissError}
onIntent={handleIntent}
pagePaths={pagePaths}
watches={watches}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,10 +409,10 @@ export function DashboardAgentPanel({
}, [watchRequest]);

// Nothing is posted or persisted until the card is submitted.
const openWatchCard = useCallback((spec: WatchSpec) => {
const openWatchCard = useCallback((spec: WatchSpec, target?: { environmentId: string }) => {
dispatchWatchCard({
type: "open",
draft: watchDraftFor(spec),
draft: watchDraftFor(spec, target),
requestId: generateFriendlyId("wreq"),
});
}, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ describe("the card's sections appear only when they have something in them", ()
expect(html).toContain("Hypotheses");
expect(html).toContain("The receipt builder is handed a null order id.");
});

it("leaves out the hypotheses count on the toggle when there are none", () => {
const html = markup({ block: block({}) });
expect(html).not.toContain("hypothesis");
expect(html).not.toContain("hypotheses");
});

it("shows the hypotheses count on the toggle once there is one", () => {
const html = markup({ block: block({ hypotheses: [HYPOTHESIS] }) });
expect(html).toContain("1 hypothesis");
});
});

describe("action buttons need a host to hand the intent to", () => {
Expand Down
Loading
Loading