Skip to content

feat(webapp): dashboard agent — UI - #4529

Open
kathiekiwi wants to merge 22 commits into
feat/dashboard-agent-flowsfrom
feat/dashboard-agent-ui
Open

feat(webapp): dashboard agent — UI#4529
kathiekiwi wants to merge 22 commits into
feat/dashboard-agent-flowsfrom
feat/dashboard-agent-ui

Conversation

@kathiekiwi

@kathiekiwi kathiekiwi commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #4418. Merge that first.

The dashboard agent's UI: the side panel, the marks that tell it which page you're on, and the entry points. #4418 works without this — the system is simply invisible.

Screenshot 2026-08-07 at 15 14 29

What's inside

  • Panel — chat, rich cards, fullscreen, transcript, quota.
  • Suggested prompts — page-aware chips; investigate chips appear only on loader-backed abnormal state.
  • Page markshandle.agentPageContext on 47 routes, ~20 lines each.
  • Entry points — Ask Trigger button, ⌘J, Help & Feedback. The old ⌘I and ?aiHelp= links keep working.

Notes

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 17a0f07

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The pull request expands the Dashboard Agent into a page-aware chat experience. It adds shared channel routing, fullscreen controls, chat history, quotas, transcript handling, structured view blocks, investigations, reports, suggested prompts, and route metadata. It removes selected page-header Docs controls and adds investigation actions for failed runs, waiting runs, and degraded queues. Tests cover routing, prompts, rendering, transcript state, quotas, navigation, accessibility, and report parity.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the dashboard agent UI and includes a screenshot, but it omits the required issue reference, checklist, testing, changelog, and screenshots sections. Add the required template sections, including the issue reference, completed checklist, testing steps, changelog entry, and screenshots section.
Docstring Coverage ⚠️ Warning Docstring coverage is 39.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main dashboard agent UI change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-agent-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 5 potential issues.

Open in Devin Review

Comment thread apps/webapp/app/hooks/useAgentPageContext.ts
Comment on lines +5 to +8
// Always undefined until billing supplies plan detection, which means no cap.
function useIsFreePlan(): boolean | undefined {
return undefined;
}

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.

🔍 Free-plan message cap is permanently inert

useIsFreePlan() always returns undefined, so resolveMessageQuota always yields { kind: "unlimited" }. That means AgentUpgradeBlock, AgentQuotaNotice, the atMessageCap guard in DashboardAgentChat and the ?quota=1 fetch are all dead paths today. Worth confirming this is intentional scaffolding (the comment says billing hasn't supplied plan detection yet) rather than a wiring omission. Note also that if the cap ever activates mid-stream, the composer — and with it the Stop button — is unmounted while a turn is still streaming.

Open in 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.

Intentional — the cap is scaffolding until billing supplies plan detection, as the comment on useIsFreePlan says, so every path behind it is inert by design. The composer/Stop ordering when the cap flips mid-turn gets settled when billing wires the flag up.

Comment thread apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 1 new potential issue.

Open in Devin Review

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

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 3 new potential issues.

Open in Devin Review

Comment thread apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx Outdated

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 2 new potential issues.

Open in Devin Review

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

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 1 new potential issue.

Open in Devin Review

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@887f5b6

trigger.dev

npm i https://pkg.pr.new/trigger.dev@887f5b6

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@887f5b6

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@887f5b6

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@887f5b6

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@887f5b6

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@887f5b6

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@887f5b6

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@887f5b6

commit: 887f5b6

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 1 new potential issue.

Open in Devin Review

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment thread apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx Outdated
@kathiekiwi
kathiekiwi force-pushed the feat/dashboard-agent-ui branch from bd4d4a0 to 887f5b6 Compare August 8, 2026 12:05

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 15

🧹 Nitpick comments (22)
apps/webapp/app/components/Shortcuts.tsx (1)

68-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Source the modifier from TOGGLE_PANEL_SHORTCUT as well.

Line 69 hardcodes ["mod"] while line 70 reads the key from the shared constant. If the modifier of TOGGLE_PANEL_SHORTCUT changes, this row displays the wrong combination. The new Chat section at lines 103-107 already reads both parts from NEW_CHAT_SHORTCUT. apps/webapp/app/components/dashboard-agent/dashboardAgentLauncher.tsx passes the whole object to ShortcutKey, so a single render is enough here.

♻️ Proposed fix
             <Shortcut name={ASK_AGENT_LABEL}>
-              <ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
-              <ShortcutKey shortcut={{ key: TOGGLE_PANEL_SHORTCUT.key }} variant="medium/bright" />
+              <ShortcutKey shortcut={TOGGLE_PANEL_SHORTCUT} variant="medium/bright" />
             </Shortcut>
apps/webapp/app/components/dashboard-agent/agent-identity.ts (2)

1-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two entry points use different agent icons.

This module defines AgentIcon as the shared agent mark, and AskAgentButton.tsx uses it. However, apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx line 133 renders AgentMonoLogo for the same ASK_AGENT_LABEL action. The agent therefore appears with two different icons across entry points. Pick one mark and export it from this module, or document why the popover differs.


3-3: 📐 Maintainability & Code Quality | 🔵 Trivial

Tracked TODO: final agent icon.

TODO(TRI-12763) records that the placeholder icon must be replaced. The placeholder ships to users in the meantime. Confirm that TRI-12763 is scheduled before this feature is enabled for all plans.

Do you want me to open a follow-up issue that links this line to TRI-12763?

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx (1)

91-94: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the handle export below the last import.

The pageMeta import at Line 94 sits after the handle declaration. The code still runs, because ES module imports are hoisted. However, this ordering is hard to read and import/first style rules flag it.

♻️ Proposed reordering
 import { sectionAgentPageContext } from "~/components/dashboard-agent/suggested-prompts";
 import type { Handle } from "~/utils/handle";
+import { pageMeta } from "~/utils/pageTitle";
 
 export const handle: Handle = {
   agentPageContext: () => sectionAgentPageContext("envvars"),
 };
-import { pageMeta } from "~/utils/pageTitle";
 
 export const meta = pageMeta("Environment variables");
apps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.ts (1)

134-157: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Guard against a non-finite burst limit.

z.number() accepts NaN and Infinity in Zod 3. If burstFactor or concurrencyLimit arrives as NaN, limit becomes NaN and the comparison silently disables the saturation signal. Use z.number().finite() for these fields to make the contract explicit.

♻️ Proposed schema tightening
 const queuesLoaderDataSchema = z.object({
   environment: z.object({
-    running: z.number(),
-    queued: z.number(),
-    concurrencyLimit: z.number(),
-    burstFactor: z.number().nullish(),
+    running: z.number().finite(),
+    queued: z.number().finite(),
+    concurrencyLimit: z.number().finite(),
+    burstFactor: z.number().finite().nullish(),
   }),
 });
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsx (1)

21-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Declare handle after the import block in four routes. Each of these routes inserts the export const handle declaration between import statements, leaving import { pageMeta } from "~/utils/pageTitle" below it. The code runs because ESM hoists imports, but the split import block is inconsistent with the other routes in this PR (query, regions, runs.$runParam, schedules.$scheduleParam, sessions.$sessionParam, queues), which declare handle after all imports.

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsx#L21-L26: move the handle declaration below the pageMeta import.
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx#L55-L60: move the handle declaration below the pageMeta import.
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx#L77-L82: move the handle declaration below the pageMeta import.
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx#L50-L55: move the handle declaration below the pageMeta import.
apps/webapp/app/components/dashboard-agent/view-actions.test.ts (1)

50-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider replacing source-text assertions with behavior tests.

These assertions read ActionsBlock.tsx as a string. A formatting change or a rename breaks them without a behavior change, and they do not prove the component renders or dispatches correctly. If a React renderer is already available in this suite, assert on rendered output and on the onIntent callback instead.

apps/webapp/app/components/dashboard-agent/AgentChart.tsx (1)

50-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared action-button row.

ChartActions duplicates ActionsBlock in apps/webapp/app/components/dashboard-agent/ActionsBlock.tsx (lines 9-31). Both filter with renderableActions, key by index, pick primary/small for the first item, and cast with as AgentIntent. One shared component keeps the styling rules and the cast in one place.

♻️ Suggested shared component
// chat-layout.tsx (or a new agent-actions.tsx)
export function AgentActionButtons({
  actions,
  onIntent,
}: {
  actions: readonly { label: string; intent: unknown }[];
  onIntent?: (intent: AgentIntent) => void;
}) {
  const renderable = renderableActions(actions);
  if (!onIntent || renderable.length === 0) return null;
  return (
    <ChatActionsRow>
      {renderable.map((action, i) => (
        <Button
          key={i}
          variant={i === 0 ? "primary/small" : "secondary/small"}
          onClick={() => onIntent(action.intent as AgentIntent)}
        >
          {action.label}
        </Button>
      ))}
    </ChatActionsRow>
  );
}
apps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx (1)

52-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider sharing the default page context.

The literal { page: { kind: "other", path: "" }, signals: [] } also appears in apps/webapp/app/components/dashboard-agent/DashboardAgentDraft.tsx. Both call sites must resolve the same prompts, and the draft comment states that intent. Export one constant from the suggested-prompts module so the two stay in sync.

♻️ Suggested change
+import {
+  DEFAULT_AGENT_PAGE_CONTEXT,
+  readDismissedPromptIds,
+  resolveSuggestedPromptsBySlot,
+  type ResolvedPromptSlot,
+} from "./suggested-prompts";
+
   const prompts = useMemo(
     () =>
-      resolveSuggestedPromptsBySlot(
-        pageContext ?? { page: { kind: "other", path: "" }, signals: [] },
-        { promoted, dismissedIds: effectiveDismissedIds }
-      ),
+      resolveSuggestedPromptsBySlot(pageContext ?? DEFAULT_AGENT_PAGE_CONTEXT, {
+        promoted,
+        dismissedIds: effectiveDismissedIds,
+      }),
     [pageContext, promoted, effectiveDismissedIds]
   );
apps/webapp/app/components/dashboard-agent/view-catalog.tsx (1)

28-31: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider precomputing the original indices.

blocks.indexOf(block) runs a linear scan for every rendered block, so the loop is O(n²). It also returns the first index if one block object instance appears twice in blocks, which yields duplicate keys. A single index map removes both concerns.

♻️ Suggested change
-      {latestRevisionBlocks(blocks).map((block) => {
-        // Index into the original array, so collapsing a revision above an
-        // envelope-less block can't shift its key.
-        const key = blockKey(block, blocks.indexOf(block));
+      {(() => {
+        // Index into the original array, so collapsing a revision above an
+        // envelope-less block can't shift its key.
+        const originalIndex = new Map(blocks.map((block, index) => [block, index]));
+        return latestRevisionBlocks(blocks).map((block) => {
+          const key = blockKey(block, originalIndex.get(block) ?? 0);
apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx (1)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider moving toSafeUrl to a shared utility module.

toSafeUrl is a URL sanitizer, but it is imported from the view module ~/components/runs/v3/agent/AgentMessageView. This file now uses it for both evidence references and documentation targets. A dedicated util module keeps the security helper independent of a rendering component.

This is a placement concern only. The current behavior is correct.

Also applies to: 114-136

apps/webapp/app/components/dashboard-agent/DashboardAgentComposer.tsx (1)

121-132: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mount the live region before the counter appears.

The element that carries aria-live="polite" is added to the DOM only when the count reaches MESSAGE_CHARS_WARN_AT. Several screen readers announce updates only for live regions that already exist in the DOM. Keep the region mounted and change its content instead.

♿ Proposed change
-      {/* Only near the limit: a normal message never sees a counter. */}
-      {value.length >= MESSAGE_CHARS_WARN_AT ? (
-        <p
-          className={cn(
-            "self-end text-xxs tabular-nums",
-            value.length >= MAX_MESSAGE_CHARS ? "text-error" : "text-text-dimmed"
-          )}
-          aria-live="polite"
-        >
-          {value.length} / {MAX_MESSAGE_CHARS}
-        </p>
-      ) : null}
+      {/* Only near the limit: a normal message never sees a counter. */}
+      <p
+        className={cn(
+          "self-end text-xxs tabular-nums",
+          value.length >= MAX_MESSAGE_CHARS ? "text-error" : "text-text-dimmed",
+          value.length < MESSAGE_CHARS_WARN_AT && "hidden"
+        )}
+        aria-live="polite"
+      >
+        {value.length >= MESSAGE_CHARS_WARN_AT ? `${value.length} / ${MAX_MESSAGE_CHARS}` : ""}
+      </p>
apps/webapp/app/components/dashboard-agent/DashboardAgentDraft.tsx (1)

29-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Share the empty page-context default and forward dismissedIds.

Two points:

  1. The fallback literal { page: { kind: "other", path: "" }, signals: [] } also exists in apps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx. Export one named constant from the suggested-prompts module and use it in both places.
  2. dismissedIds is not passed to DashboardAgentHero, so DashboardAgentSuggestedPrompts reads localStorage a second time. Forward the value the draft already read to keep one source.

As per coding guidelines: "Use named constants for sentinel or placeholder values instead of scattering raw string literals across comparisons."

♻️ Proposed change
   const placeholderSuggestion = useMemo(
     () =>
-      resolveSuggestedPromptsBySlot(
-        pageContext ?? { page: { kind: "other", path: "" }, signals: [] },
-        {
-          promoted: promotedPrompt,
-          dismissedIds,
-        }
-      )[0]?.prompt.prompt,
+      resolveSuggestedPromptsBySlot(pageContext ?? EMPTY_PAGE_CONTEXT, {
+        promoted: promotedPrompt,
+        dismissedIds,
+      })[0]?.prompt.prompt,
     [pageContext, promotedPrompt, dismissedIds]
   );
@@
     <DashboardAgentHero
       onSelect={submit}
       pageContext={pageContext}
       promoted={promotedPrompt}
+      dismissedIds={dismissedIds}

Source: Coding guidelines

apps/webapp/app/components/dashboard-agent/report-block-adapter.test.ts (2)

94-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The assertion does not prove that series is dropped.

The test name states that the curated output drops links and series. The vm fixture has no series on its metric, so expect(block.vm.metrics[0]!.series).toBeUndefined() passes even if the adapter kept the field. Add series to the metric in the input to make the assertion meaningful.

💚 Proposed change
   it("accepts the curated tool output, which drops links and series", () => {
     const { links, ...curated } = vm;
-    const block = reportBlockFromToolPart(part({ output: { ...curated, seriesOmitted: true } }))!;
+    const block = reportBlockFromToolPart(
+      part({
+        output: {
+          ...curated,
+          metrics: [{ ...curated.metrics[0]!, series: [1, 2, 3] }],
+          seriesOmitted: true,
+        },
+      })
+    )!;
     expect(block.vm.links).toEqual([]);
     expect(block.vm.metrics[0]!.series).toBeUndefined();

118-127: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

sameKeyAgain uses a different tool call id.

The variable name states that the key repeats, but the call passes toolCallId: "call_3". The assertion therefore repeats the distinct-id case instead of covering a repeated id. Rename the variable, or pass "call_1" and assert the expected collapse behavior.

apps/webapp/app/components/dashboard-agent/useAgentMessageQuota.ts (1)

5-8: 📐 Maintainability & Code Quality | 🔵 Trivial

useIsFreePlan always returns undefined, so the whole quota path is inert.

resolveMessageQuota returns { kind: "unlimited" } whenever isFreePlan !== true. The effect at Line 25 also returns early, so the quota endpoint is never called. As a result AgentUpgradeBlock and AgentQuotaNotice in DashboardAgentChat.tsx never render, and atMessageCap is always false.

The comment records this as intentional until billing supplies plan detection. Confirm that shipping the free-plan cap as inactive is the intended state for this PR. I can open a follow-up issue to track wiring plan detection if that helps.

apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx (2)

11-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the registered shortcut constant instead of redefining it.

NEW_CHAT_SHORTCUT duplicates the ⌘J definition that DashboardAgent registers as TOGGLE_PANEL_SHORTCUT. If one definition changes, the displayed key and the registered key diverge. Export the single source from the module that registers it, and import it here for display.


57-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Accessible name hides the visible label.

The trigger renders the chat title as visible text, but aria-label="Chat history" replaces that name. Speech-input users cannot activate the control by the visible text. Include the title in the accessible name.

♿ Proposed change
-          aria-label="Chat history"
+          aria-label={`Chat history: ${title}`}
           title={title}
apps/webapp/app/components/dashboard-agent/ask-ai-channels.ts (1)

51-63: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider rejecting absolute values for environmentPath.

new URL(environmentPath, origin) ignores origin when environmentPath is absolute. The result then feeds redirect(...) in apps/webapp/app/routes/projects.$projectRef.ai-help.ts. The current caller passes a builder-generated internal path, so there is no exploit today. A guard keeps the function safe if a future caller forwards request data.

🛡️ Proposed guard
   const url = new URL(environmentPath, origin);
+  if (url.origin !== new URL(origin).origin) {
+    throw new Error("environmentPath must be relative to the given origin");
+  }
   url.searchParams.set(ASK_AI_DEEP_LINK_PARAM, query);
apps/webapp/app/components/dashboard-agent/InvestigationCard.test.ts (1)

38-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider relaxing the exact-source assertion.

Line 39 matches one exact source line, including spacing. Prettier reflow, a rename of onIntent or actions, or an equivalent early return breaks this test without any behavior change. The other assertions in this file are pattern-based and tolerate that. Consider matching a looser pattern, or asserting the behavior by rendering the component with and without onIntent.

♻️ Looser pattern
-    expect(source).toMatch(/if \(!onIntent \|\| actions\.length === 0\) return null;/);
+    expect(source).toMatch(/!onIntent[\s\S]{0,40}return null/);
apps/webapp/app/components/dashboard-agent/report-sparkline.tsx (2)

468-475: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The Math.max(..., 1) guard sits on the wrong operand.

Line 472 applies Math.max(..., 1) to the slice end index. The guard reads as protection against an empty slice, but it cannot help: the function returns early when points.length <= maxBars, so perBar > 1 and Math.floor((i + 1) * perBar) is always at least 1. The expression is dead and misleading. The real empty-slice guard is already on line 473.

♻️ Proposed simplification
-    const slice = points.slice(Math.floor(i * perBar), Math.max(Math.floor((i + 1) * perBar), 1));
+    const slice = points.slice(Math.floor(i * perBar), Math.floor((i + 1) * perBar));
     return slice.reduce((sum, v) => sum + v, 0) / Math.max(slice.length, 1);

528-544: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Date.now() during render breaks the stated purity of this file.

The file header states no Remix hooks and no host state, and ReportView.test.ts enforces that for the sibling card. Line 532 reads the wall clock during render. Two consequences follow. Server and client renders produce different date values for the same data. Every re-render shifts the synthesised bar timestamps, so a tooltip can report a different time for the same bar.

The bars themselves do not depend on the clock, only the tooltip labels do. Consider passing the series end time in through the view model, so the timestamps come from the data instead of the render.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c2923ee0-77ad-49e0-97df-d95110a2b67d

📥 Commits

Reviewing files that changed from the base of the PR and between e2704ea and 887f5b6.

📒 Files selected for processing (155)
  • .server-changes/dashboard-agent.md
  • apps/webapp/app/components/AskAI.tsx
  • apps/webapp/app/components/BlankStatePanels.tsx
  • apps/webapp/app/components/Shortcuts.tsx
  • apps/webapp/app/components/dashboard-agent/ActionsBlock.tsx
  • apps/webapp/app/components/dashboard-agent/AgentChart.tsx
  • apps/webapp/app/components/dashboard-agent/AgentUpgradeGate.tsx
  • apps/webapp/app/components/dashboard-agent/AskAgentButton.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentComposer.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentContextBanner.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentDraft.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentHero.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentHistory.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx
  • apps/webapp/app/components/dashboard-agent/InvestigateButton.tsx
  • apps/webapp/app/components/dashboard-agent/InvestigationCard.test.ts
  • apps/webapp/app/components/dashboard-agent/InvestigationCard.tsx
  • apps/webapp/app/components/dashboard-agent/ReportView.test.ts
  • apps/webapp/app/components/dashboard-agent/ReportView.tsx
  • apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx
  • apps/webapp/app/components/dashboard-agent/agent-badges.tsx
  • apps/webapp/app/components/dashboard-agent/agent-card.tsx
  • apps/webapp/app/components/dashboard-agent/agent-identity.ts
  • apps/webapp/app/components/dashboard-agent/agent-shortcuts.test.ts
  • apps/webapp/app/components/dashboard-agent/ask-ai-channels.test.ts
  • apps/webapp/app/components/dashboard-agent/ask-ai-channels.ts
  • apps/webapp/app/components/dashboard-agent/chat-layout.test.ts
  • apps/webapp/app/components/dashboard-agent/chat-layout.tsx
  • apps/webapp/app/components/dashboard-agent/dashboardAgentLauncher.tsx
  • apps/webapp/app/components/dashboard-agent/dashboardAgentOpenRequest.ts
  • apps/webapp/app/components/dashboard-agent/demo/fixtures/page-context.ts
  • apps/webapp/app/components/dashboard-agent/demo/fixtures/reports.ts
  • apps/webapp/app/components/dashboard-agent/demo/ids.ts
  • apps/webapp/app/components/dashboard-agent/diagnosis-actions.test.ts
  • apps/webapp/app/components/dashboard-agent/diagnosis-actions.ts
  • apps/webapp/app/components/dashboard-agent/investigate-prompts.test.ts
  • apps/webapp/app/components/dashboard-agent/investigate-prompts.ts
  • apps/webapp/app/components/dashboard-agent/investigation-winners.test.ts
  • apps/webapp/app/components/dashboard-agent/investigation-winners.ts
  • apps/webapp/app/components/dashboard-agent/list-row.tsx
  • apps/webapp/app/components/dashboard-agent/message-order.test.ts
  • apps/webapp/app/components/dashboard-agent/message-order.ts
  • apps/webapp/app/components/dashboard-agent/message-quota.test.ts
  • apps/webapp/app/components/dashboard-agent/message-quota.ts
  • apps/webapp/app/components/dashboard-agent/model-markdown.test.ts
  • apps/webapp/app/components/dashboard-agent/model-markdown.ts
  • apps/webapp/app/components/dashboard-agent/navigate-target.test.ts
  • apps/webapp/app/components/dashboard-agent/navigate-target.ts
  • apps/webapp/app/components/dashboard-agent/opened-chat.test.ts
  • apps/webapp/app/components/dashboard-agent/opened-chat.ts
  • apps/webapp/app/components/dashboard-agent/page-context-types.ts
  • apps/webapp/app/components/dashboard-agent/page-label.test.ts
  • apps/webapp/app/components/dashboard-agent/page-label.ts
  • apps/webapp/app/components/dashboard-agent/panel-layout.tsx
  • apps/webapp/app/components/dashboard-agent/pending-intents.test.ts
  • apps/webapp/app/components/dashboard-agent/pending-intents.ts
  • apps/webapp/app/components/dashboard-agent/progress-line.test.ts
  • apps/webapp/app/components/dashboard-agent/progress-line.ts
  • apps/webapp/app/components/dashboard-agent/report-block-adapter.test.ts
  • apps/webapp/app/components/dashboard-agent/report-block-adapter.ts
  • apps/webapp/app/components/dashboard-agent/report-sparkline.tsx
  • apps/webapp/app/components/dashboard-agent/run-id.test.ts
  • apps/webapp/app/components/dashboard-agent/run-id.ts
  • apps/webapp/app/components/dashboard-agent/settled-transcript.test.ts
  • apps/webapp/app/components/dashboard-agent/settled-transcript.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/dismissal.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/docs-prompts.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/index.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.test.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/page-prompts.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/promoted.test.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/promoted.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/promotedPrompt.server.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/prompt-chips.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/registry.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/resolver.test.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/resolver.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/signal-prompts.ts
  • apps/webapp/app/components/dashboard-agent/tool-labels.test.ts
  • apps/webapp/app/components/dashboard-agent/tool-labels.ts
  • apps/webapp/app/components/dashboard-agent/turn-error.test.ts
  • apps/webapp/app/components/dashboard-agent/turn-error.ts
  • apps/webapp/app/components/dashboard-agent/useAgentMessageQuota.ts
  • apps/webapp/app/components/dashboard-agent/useTranscriptAutoScroll.ts
  • apps/webapp/app/components/dashboard-agent/useTriggerUriResolver.ts
  • apps/webapp/app/components/dashboard-agent/view-actions.test.ts
  • apps/webapp/app/components/dashboard-agent/view-actions.ts
  • apps/webapp/app/components/dashboard-agent/view-blocks.test.ts
  • apps/webapp/app/components/dashboard-agent/view-blocks.ts
  • apps/webapp/app/components/dashboard-agent/view-catalog.tsx
  • apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/hooks/useAgentPageContext.ts
  • apps/webapp/app/hooks/useAskAiAvailability.ts
  • apps/webapp/app/hooks/useShortcutKeys.tsx
  • apps/webapp/app/root.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev-branches/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.compare/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam/route.tsx
  • apps/webapp/app/routes/_app/route.tsx
  • apps/webapp/app/routes/projects.$projectRef.ai-help.ts
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
  • apps/webapp/app/utils/handle.ts
  • apps/webapp/test/dashboardAgentInvestigationSweepCard.test.ts
  • apps/webapp/test/reportRenderParity.test.ts
  • apps/webapp/test/taskCodeSnippets.test.ts
💤 Files with no reviewable changes (1)
  • apps/webapp/app/root.tsx

Comment thread apps/webapp/app/components/dashboard-agent/AskAgentButton.tsx Outdated
Comment thread apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx Outdated
Comment thread apps/webapp/app/components/dashboard-agent/DashboardAgentHero.tsx Outdated
Comment thread apps/webapp/app/components/dashboard-agent/useTriggerUriResolver.ts Outdated
Comment thread apps/webapp/app/env.server.ts Outdated
Comment thread apps/webapp/test/reportRenderParity.test.ts
The panel, the page-context marks on the pages the agent reads, and the entry points.
Ask AI (Kapa) owns the two entry points it had before the dashboard agent
replaced it: Cmd-I, and the `?aiHelp=` deep link the CLI's "Get a fix for
this error using AI" line points at. `AskAIRoot` mounts in the `_app`
layout, above every signed-in page, so Cmd-I reaches it from org-level
pages too and the dialog outlives whatever opened it.

The agent no longer reads deep links at all: nothing produced its `?ask=`
param except the CLI redirect, and both readers consume the param, so a
live agent reader would always beat Kapa to it. It stays the fall-through
— where Kapa cannot open (self-hosted, or no website id), both channels
land on the agent instead of dead-ending.
…the pipeline emits

The fixture still set `facts.staleReason`, renamed to `untrustworthyReason` three commits
before the caveat started reading it, so the branch's only trust snapshot fell back to
"could not be verified" for a report whose reason is known.
The scheduled example lost its import line to the standard one, so copying
it gave code that does not compile.
Without org/project/env context the run button rendered but did nothing.
Shortcuts can now ask for the browser default to be prevented, and the
agent's keystroke does.
Selecting a stored chat with no messages dropped you into a fresh draft,
as if the chat had been deleted.
Radix tooltip content is not the accessible name of its trigger, so the
icon-only ask-agent button and the two deploy docs links announced as
unnamed controls. Name them explicitly and pass asChild so the tooltip
trigger stops wrapping them in a second button.

Adds a source scan that fails on the next SimpleTooltip with an unnamed
or double-wrapped control, with the pre-existing sites baselined.
A limit of 0 is zero capacity, not saturation: running >= 0 holds for every
queue, so any backlog marked the queue degraded and offered Investigate, while
the agent's own suggested prompt stayed silent. One predicate now decides it
for the queue detail page, the queues list badge and the page mappers.
Retry appended the last user message again, so the failed turn stayed in the
transcript and its text was sent twice. It now regenerates once the agent has
started answering, and otherwise re-sends the failed turn under its own id.
…e reader's clock

Bar timestamps came from Date.now() during render, so the same bar reported a
different time on every re-render and a server pass disagreed with the client.
They now come from the view model's generatedAt, which the schema already
describes as the timestamp the renderer must not invent.

Moves the arithmetic into report-spark.ts to keep it clock-free and testable,
and drops the unreachable Math.max on the slice end while doing so.
ViewBlocks looked each surviving block's index up with indexOf inside the render
loop: quadratic, and two occurrences of the same block object both answered with
the first index, so they collided on one React key. latestRevisionEntries carries
each survivor's position out instead.
setSearchParams only starts the navigation that drops the param, so a render
before it commits saw the question again and asked it a second time. The reader
now records what it sent and forgets it once the URL no longer carries it, so a
later visit with the same question still works.
A request still in flight at unmount rejected afterwards, and the catch scheduled
a retry that fetched again and set state for a component that was gone. The hook
tracks whether it is still mounted and neither records nor reschedules once it is
not.
The character counter's live region only entered the DOM at the warning point,
and several screen readers only announce updates for a region that was already
there; it is now always mounted and empty until there is something to say.

The history trigger's aria-label replaced the chat title it shows, so a
speech-input user could not activate it by the words on it. The title now leads
the accessible name.
…anel

justify-center on a scrolling column overflows equally in both directions, and
nothing can scroll back past the origin, so at the docked panel's narrowest the
heading and composer were unreachable. The child centres with m-auto, which gives
its space up once there is none to spare.
new URL(environmentPath, origin) ignores origin when the path is absolute, and
the result goes straight into redirect(). Today's only caller passes a
builder-generated internal path, so this closes the gap rather than a hole.
@kathiekiwi
kathiekiwi force-pushed the feat/dashboard-agent-ui branch from 887f5b6 to 17a0f07 Compare August 8, 2026 14:30

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (7)
apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx (1)

267-282: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cancel the settle poll when the component unmounts or a new turn starts.

pollSettledTranscript runs up to the full delay sequence with no cancellation. Two cases follow from this:

  • The user closes the panel. The poll keeps fetching ${actionPath}?chatId=... and calls setMessages on an unmounted chat.
  • The user sends another message while the poll is in flight. A late merge can apply a stale server transcript over the newer local state.

Add a cancellation flag in the effect cleanup and check it before each apply.

♻️ Proposed cancellation guard
     onTurnSettled();
     // The terminal card is written to the chat row after the stream closes, so this
     // mounted panel would otherwise keep showing the last `in_progress` revision.
     if (!hasOpenInvestigation(messagesRef.current)) return;
+    let cancelled = false;
     void pollSettledTranscript<UIMessage>({
       fetchTranscript: () => fetchChatTranscript(actionPath, chatId),
-      apply: (merge) => setMessages((current) => merge(current)),
+      apply: (merge) => {
+        if (cancelled) return;
+        setMessages((current) => merge(current));
+      },
       wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
     });
+    return () => {
+      cancelled = true;
+    };
   }, [status, onTurnSettled, actionPath, chatId, setMessages]);
apps/webapp/app/components/dashboard-agent/DashboardAgentComposer.tsx (1)

98-108: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Announce the Tab affordance.

Tab inserts placeholderSuggestion only while the field is empty. Nothing tells the user that this key does that. Screen-reader users hear the placeholder as the field value hint, with no mention of the shortcut.

Extend the accessible description so the affordance is discoverable.

♻️ Proposed hint
             placeholder={placeholderSuggestion ?? "Type a message…"}
             aria-label="Message the dashboard agent"
+            aria-describedby={placeholderSuggestion ? suggestionHintId : undefined}

Then render the hint next to the live region:

{placeholderSuggestion ? (
  <p id={suggestionHintId} className="sr-only">
    Press Tab to use the suggested prompt.
  </p>
) : null}
apps/webapp/app/components/dashboard-agent/investigation-winners.test.ts (1)

45-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the source-text assertions whitespace-tolerant.

These assertions match the literal source of DashboardAgentMessages.tsx. A formatter run that wraps useMemo(() => winningInvestigationOccurrences(messages) onto two lines breaks the test without any behavior change. The same applies to the two toContain calls.

Normalize whitespace before matching.

♻️ Proposed normalization
-const source = readFileSync(new URL("./DashboardAgentMessages.tsx", import.meta.url), "utf8");
+const source = readFileSync(new URL("./DashboardAgentMessages.tsx", import.meta.url), "utf8").replace(
+  /\s+/g,
+  " "
+);

The remaining behavioral tests in this file stay unchanged.

apps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.test.ts (1)

215-218: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The test name does not match the fixture.

The name says "idle under its limit". The fixture sets running: 10 against concurrencyLimit: 10, so the queue is at its limit, not under it. The assertion is correct: nothing is queued, so no saturation signal follows. Rename the test to describe that case.

✏️ Proposed rename
-  it("emits no saturation signal when the queue is idle under its limit", () => {
+  it("emits no saturation signal at the limit with nothing waiting", () => {
apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts (1)

210-212: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Run the repository scan inside the tests, not at module load.

tsxFiles(APP_DIR) walks every .tsx file under app/ and parses each one when the module is imported. Two consequences follow:

  • If readdirSync or a parse throws, Vitest reports a collection error for the file. No test name points at the cause.
  • The full parse cost is paid even when a single test is selected with -t.

Compute the violations lazily in a beforeAll, or in a memoized helper the tests call.

♻️ Proposed lazy scan
-const violations = tsxFiles(APP_DIR).flatMap((file) =>
-  scanFile(file, path.relative(WEBAPP_DIR, file).split(path.sep).join("/"))
-);
+let cached: Violation[] | undefined;
+function allViolations(): Violation[] {
+  cached ??= tsxFiles(APP_DIR).flatMap((file) =>
+    scanFile(file, path.relative(WEBAPP_DIR, file).split(path.sep).join("/"))
+  );
+  return cached;
+}

Then replace each violations reference in the three tests with allViolations().

apps/webapp/app/components/dashboard-agent/dashboardAgentOpenRequest.ts (1)

83-86: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Keep host registration stable across openWith identity changes.

The registration effect depends on openWith and setOpen. If a caller passes new function identities each render, the effect unregisters and re-registers on every render. Each cycle calls notifyAvailability() twice, so useDashboardAgentAvailable briefly reports false and every entry point can flicker. Store the callbacks in a ref and register once per enabled change.

♻️ Proposed refactor
+  const callbacks = useRef({ openWith, setOpen });
+  callbacks.current = { openWith, setOpen };
+
   useEffect(() => {
     if (!enabled) return;
-    return registerDashboardAgentHost(({ prompt }) => (prompt ? openWith(prompt) : setOpen(true)));
-  }, [enabled, openWith, setOpen]);
+    return registerDashboardAgentHost(({ prompt }) =>
+      prompt ? callbacks.current.openWith(prompt) : callbacks.current.setOpen(true)
+    );
+  }, [enabled]);
apps/webapp/app/components/dashboard-agent/report-sparkline.tsx (1)

581-596: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Align reportDelta with the LayoutDelta contract.

LayoutDelta already has { text, dir }, so ReportView passes the correct delta shape to ReportMetricRow. reportDelta accepts a different { dir, mult?, hasBaseline } shape and has no call sites, so remove the unused ReportDelta exports/helper.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9777d194-a07c-40e7-b11d-48101749dab7

📥 Commits

Reviewing files that changed from the base of the PR and between 887f5b6 and 17a0f07.

📒 Files selected for processing (39)
  • apps/webapp/app/components/BlankStatePanels.tsx
  • apps/webapp/app/components/dashboard-agent/AskAgentButton.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentComposer.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentHero.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsx
  • apps/webapp/app/components/dashboard-agent/ReportView.tsx
  • apps/webapp/app/components/dashboard-agent/ask-ai-channels.test.ts
  • apps/webapp/app/components/dashboard-agent/ask-ai-channels.ts
  • apps/webapp/app/components/dashboard-agent/chat-layout.test.ts
  • apps/webapp/app/components/dashboard-agent/dashboardAgentOpenRequest.test.ts
  • apps/webapp/app/components/dashboard-agent/dashboardAgentOpenRequest.ts
  • apps/webapp/app/components/dashboard-agent/header-labels.test.ts
  • apps/webapp/app/components/dashboard-agent/header-labels.ts
  • apps/webapp/app/components/dashboard-agent/investigation-winners.test.ts
  • apps/webapp/app/components/dashboard-agent/message-limits.test.ts
  • apps/webapp/app/components/dashboard-agent/message-limits.ts
  • apps/webapp/app/components/dashboard-agent/report-spark.test.ts
  • apps/webapp/app/components/dashboard-agent/report-spark.ts
  • apps/webapp/app/components/dashboard-agent/report-sparkline.tsx
  • apps/webapp/app/components/dashboard-agent/resolve-uris.test.ts
  • apps/webapp/app/components/dashboard-agent/resolve-uris.ts
  • apps/webapp/app/components/dashboard-agent/retry-action.test.ts
  • apps/webapp/app/components/dashboard-agent/retry-action.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.test.ts
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.ts
  • apps/webapp/app/components/dashboard-agent/tooltip-accessible-name.test.ts
  • apps/webapp/app/components/dashboard-agent/useTriggerUriResolver.ts
  • apps/webapp/app/components/dashboard-agent/view-blocks.test.ts
  • apps/webapp/app/components/dashboard-agent/view-blocks.ts
  • apps/webapp/app/components/dashboard-agent/view-catalog.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/components/queues/queue-thresholds.test.ts
  • apps/webapp/app/components/queues/queue-thresholds.ts
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx
  • apps/webapp/vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/components/dashboard-agent/view-catalog.tsx
  • apps/webapp/app/components/dashboard-agent/view-blocks.test.ts
  • apps/webapp/app/components/BlankStatePanels.tsx
  • apps/webapp/app/routes/app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.$queueParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx
  • apps/webapp/app/components/dashboard-agent/ask-ai-channels.test.ts
  • apps/webapp/app/components/dashboard-agent/AskAgentButton.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentHero.tsx
  • apps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsx
  • apps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.ts

Comment on lines +13 to +20
/**
* What the composer's live region says at this length: empty until the counter is worth
* showing. The region itself stays mounted whatever this returns — several screen readers only
* announce changes to a region that was already in the DOM.
*/
export function messageCountAnnouncement(length: number): string {
return length >= MESSAGE_CHARS_WARN_AT ? `${length} / ${MAX_MESSAGE_CHARS}` : "";
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The live region announces on every keystroke past the threshold.

MESSAGE_CHARS_WARN_AT is 7200 and MAX_MESSAGE_CHARS is 8000. Above the threshold, this function returns a new string for each character typed, so the polite live region queues up to 800 announcements. Screen readers read them in order, which buries the composer's own feedback.

The returned text also carries no unit, so it is read as a bare fraction.

Announce at coarse steps and name the unit.

♻️ Proposed step-based announcement
 export function messageCountAnnouncement(length: number): string {
-  return length >= MESSAGE_CHARS_WARN_AT ? `${length} / ${MAX_MESSAGE_CHARS}` : "";
+  if (length < MESSAGE_CHARS_WARN_AT) return "";
+  if (length >= MAX_MESSAGE_CHARS) return `Message limit reached: ${MAX_MESSAGE_CHARS} characters.`;
+  // Coarse steps: a per-keystroke announcement floods the polite queue.
+  const remaining = MAX_MESSAGE_CHARS - length;
+  const step = Math.ceil(remaining / 100) * 100;
+  return `About ${step} characters left.`;
 }

Update apps/webapp/app/components/dashboard-agent/message-limits.test.ts to match the new strings.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* What the composer's live region says at this length: empty until the counter is worth
* showing. The region itself stays mounted whatever this returns several screen readers only
* announce changes to a region that was already in the DOM.
*/
export function messageCountAnnouncement(length: number): string {
return length >= MESSAGE_CHARS_WARN_AT ? `${length} / ${MAX_MESSAGE_CHARS}` : "";
}
/**
* What the composer's live region says at this length: empty until the counter is worth
* showing. The region itself stays mounted whatever this returns several screen readers only
* announce changes to a region that was already in the DOM.
*/
export function messageCountAnnouncement(length: number): string {
if (length < MESSAGE_CHARS_WARN_AT) return "";
if (length >= MAX_MESSAGE_CHARS) return `Message limit reached: ${MAX_MESSAGE_CHARS} characters.`;
// Coarse steps: a per-keystroke announcement floods the polite queue.
const remaining = MAX_MESSAGE_CHARS - length;
const step = Math.ceil(remaining / 100) * 100;
return `About ${step} characters left.`;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant