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

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 661eba3f-431b-4965-bd6d-00af5c954aad

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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, and suggested prompts. Routes now provide agent page context and investigation actions. Documentation links are removed from selected page headers. Tests cover rendering, routing, prompts, navigation, transcript state, quotas, and report parity.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly identifies the main change: the dashboard agent UI for the webapp.
Description check ✅ Passed The description explains the main UI changes, entry points, gating, and compatibility, but omits the template checklist and explicit testing details.
✨ 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
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