Skip to content

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

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

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

Conversation

@kathiekiwi

@kathiekiwikathiekiwi 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-botBot commented Aug 7, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ddb9f3c

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

coderabbitaiBot 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: e7205ab6-2e37-4543-a970-224ce8a756de

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, 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 nameStatusExplanationResolution
Description check⚠️ WarningThe 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⚠️ WarningDocstring 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 nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the main dashboard agent UI change.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-newBot 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[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@kathiekiwi
kathiekiwiforce-pushed the feat/dashboard-agent-ui branch from bd4d4a0 to 887f5b6CompareAugust 8, 2026 12:05
coderabbitai[bot]

This comment was marked as resolved.

@kathiekiwi
kathiekiwiforce-pushed the feat/dashboard-agent-ui branch from 887f5b6 to 17a0f07CompareAugust 8, 2026 14:30
coderabbitai[bot]

This comment was marked as resolved.

@kathiekiwi
kathiekiwiforce-pushed the feat/dashboard-agent-ui branch from 17a0f07 to 2460d14CompareAugust 8, 2026 17:49
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.
A preview branch named `env` put a second `env` segment in the path and
`lastIndexOf` picked it, shifting every index derived from it.
… keystroke
Between the warning point and the limit the region announced a new count
per character; it now steps in 200s and names the limit on reaching it.
@kathiekiwi
kathiekiwiforce-pushed the feat/dashboard-agent-ui branch from 2460d14 to 9447128CompareAugust 8, 2026 18:08

@devin-ai-integrationdevin-ai-integrationBot 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 on lines +33 to +39
<AskAIRoot>
{() => (
<AppContainer>
<Outlet />
</AppContainer>
)}
</AskAIRoot>

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.

🔴 Whole dashboard is torn down and rebuilt right after every page load on cloud

The entire signed-in dashboard is placed inside a helper that only renders its real content after the page becomes interactive (<AskAIRoot> at apps/webapp/app/routes/_app/route.tsx:33-39), so on managed cloud every page is thrown away and rebuilt from scratch immediately after it loads.

Impact: On trigger.dev cloud, every page load discards and recreates the whole dashboard once — page state resets, one-time startup work runs twice (duplicate fetches, re-opened live subscriptions), and the page can visibly flash.

Why the subtree remounts: ClientOnly swaps the element type at this position

AskAIRoot (apps/webapp/app/components/AskAI.tsx:93-108) renders <ClientOnly fallback={<>{children(undefined)}</>}>{() => <AskAIRootProvider …>{children}</AskAIRootProvider>}</ClientOnly> whenever askAiCanOpen(availability) is true (managed cloud with KAPA_AI_WEBSITE_ID set).

ClientOnly renders the fallback during SSR/hydration and then, in an effect, renders children(). At that slot the element type changes from a Fragment (holding <AppContainer><Outlet/></AppContainer>) to <AskAIRootProvider>, so React unmounts the whole subtree and mounts a fresh one. Every component under _app — i.e. the entire dashboard, including the agent panel host, Electric/SSE subscribers and any mount-time fetch — is unmounted and re-mounted once per page load.

Before this PR AskAIRoot only wrapped the Help & Feedback popover, so the same swap affected a tiny subtree.

A stable-tree arrangement avoids it, e.g. render the app normally and mount AskAIRoot beside it:

<AppContainer><AskAIRoot>{()=>null}</AskAIRoot><Outlet/></AppContainer>

(or hoist the Kapa provider so it is always the same component type on both renders).

Prompt for agents
In apps/webapp/app/routes/_app/route.tsx the whole signed-in app (`<AppContainer><Outlet/></AppContainer>`) is passed as the children callback of `AskAIRoot`. `AskAIRoot` (apps/webapp/app/components/AskAI.tsx) renders that callback inside `ClientOnly`, whose fallback is `<>{children(undefined)}</>`. Because the element type at that position changes from a Fragment to `AskAIRootProvider` once hydration completes, React unmounts and remounts the entire app subtree once per page load on managed cloud (where askAiCanOpen is true). That resets all component state and re-runs every mount effect (duplicate fetches, re-established realtime subscriptions).
Fix by keeping the element tree stable across the ClientOnly swap. Options: mount AskAIRoot as a sibling that renders nothing (`<AskAIRoot>{() => null}</AskAIRoot>` next to the Outlet), or restructure AskAIRoot so the same component wraps the children in both the fallback and the hydrated branch (e.g. always render a provider component that internally no-ops until hydrated).
Open in Devin Review

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

Comment on lines +215 to +223
try {
const res = await fetch(actionPath, { method: "POST", body });
const data = (await res.json()) as { path?: string };
if (!res.ok || !data.path) throw new Error(`Resolve failed (${res.status})`);
navigate(appendRunFilters(data.path, intent.filters));
} catch (error) {
console.error("Dashboard agent: failed to resolve a navigate target", error);
toast.error("Couldn't open that page.");
}

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.

🟡 Agent links to a source file send the user to a broken in-app page

When the agent offers to open a source file, the destination is handed to the in-app router as if it were a dashboard page (navigate(...) at apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx:219) even though the server said it points at an external site, so the user lands on a not-found page instead of the file.

Impact: Clicking such a suggestion navigates to a broken URL inside the dashboard rather than opening the linked file.

The resolver's `external` flag is dropped by the client

resolveTriggerUri returns { url, external: true } with an absolute https://github.com/... URL for trigger://…/source/… URIs (apps/webapp/app/services/resolveTriggerUri.server.ts:140-151), and the resolve action forwards external in its JSON (apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts:334-339).

goTo reads only data.path and calls React Router's navigate(), which treats an absolute URL string as an in-app path and resolves it relative to the current location. A navigate intent may carry any grammar-valid trigger:// URI (the actions-block schema validates only that it is a trigger URI — internal-packages/dashboard-agent-contracts/src/blocks.ts:104-124), so a source target is reachable whenever code-mode tools hand the model one.

Additionally, appendRunFilters would strip the origin from such an absolute URL (apps/webapp/app/components/dashboard-agent/navigate-target.ts:8-32) if filters were present.

Fix: read external from the resolve response and, when set, open it with window.open/location.assign (or render it as a link) instead of navigate().

Prompt for agents
In DashboardAgentChat.tsx `goTo`, the `resolve` action's response is parsed as `{ path?: string }` and always passed to React Router's `navigate()`. The server also returns `external: true` with an absolute off-origin URL for `trigger://…/source/…` targets (services/resolveTriggerUri.server.ts). Navigating to an absolute URL through the SPA router resolves it as an in-app path and produces a broken navigation; `appendRunFilters` would also strip its origin.
Parse `external` (and ideally `label`) from the response and branch: for external destinations open the URL in a new tab (window.open with noopener) or assign location, and only call `navigate()` (with appendRunFilters) for same-origin dashboard paths.
Open in Devin Review

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

@devin-ai-integrationdevin-ai-integrationBot 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 on lines 130 to +147
const loadHistory = useCallback(async () => {
const res = await fetch(actionPath);
if (res.ok) {
const data = (await res.json()) as { chats?: DashboardAgentChatListItem[] };
setChats(data.chats ?? []);
}
}, [actionPath]);
if (historyInFlight.current) return historyInFlight.current;
const request = (async () => {
try {
const res = await fetch(actionPath);
if (!res.ok) throw new Error(`History request failed (${res.status})`);
const data = (await res.json()) as { chats?: DashboardAgentChatListItem[] };
setChats(data.chats ?? []);
} catch (error) {
console.error("Dashboard agent: failed to load chat history", error);
toast.error("We couldn't load your previous chats. Try again in a moment.");
} finally {
historyInFlight.current = null;
}
})();
historyInFlight.current = request;
return request;
}, [actionPath, toast]);

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.

🟡 Chat list can miss the newly created chat and updated titles after a reply finishes

A request to reload the chat list is silently skipped (if (historyInFlight.current) return historyInFlight.current; at apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx:131) whenever another reload is still running, so a refresh asked for at the end of a reply can be answered with results fetched before that reply existed.

Impact: The chat list in the panel header can keep showing a stale name, or omit the chat that was just started, until something else triggers a reload.

How the de-duplication loses the newer request

loadHistory returns the in-flight promise instead of scheduling a follow-up fetch. It is called from several places that care about different points in time: panel mount, opening the history menu (apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx:341), after a delete (:315), and — the important one — onTurnSettled when a turn finishes (:373, wired to the settle effect in apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx:273).

If a mount-time or menu-open fetch is still in flight when the first turn settles, the settle-triggered refresh resolves against data captured before the chat row / generated title was written, and no further fetch is issued. A "coalesce into a trailing refetch" pattern (mark that another run is wanted and re-run once the current one finishes) avoids this.

Prompt for agents
`loadHistory` in DashboardAgentPanel.tsx de-duplicates concurrent calls by returning the in-flight promise. That drops any refresh requested while a fetch is running, including the one fired by `onTurnSettled` when the first turn of a new chat completes — which is exactly when the chat row and generated title become visible. Change the de-duplication to coalesce into a trailing refetch: keep the in-flight guard, but record that another run was requested and re-run once the current one settles, so the last requester always sees fresh data.
Open in Devin Review

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

@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { hotkeyOptions } from "~/hooks/useShortcutKeys";
import { LEGACY_ASK_AI_SHORTCUT, TOGGLE_PANEL_SHORTCUT } from "./dashboardAgentLauncher";

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.

🔍 Test imports a symbol that no longer exists

agent-shortcuts.test.ts imports LEGACY_ASK_AI_SHORTCUT from ./dashboardAgentLauncher, but that module only exports TOGGLE_PANEL_SHORTCUT (see apps/webapp/app/components/dashboard-agent/dashboardAgentLauncher.tsx:7-15). The ⌘I shortcut was moved to ASK_AI_SHORTCUT in apps/webapp/app/components/dashboard-agent/ask-ai-channels.ts:11. The suite will fail to run as written.

Open in Devin Review

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

Comment on lines +16 to +19
export function isQueueAtCapacity({ running, queued, limit }: QueueCapacity): boolean {
if (limit === null || limit === undefined || limit <= 0) return false;
return running >= limit && queued > 0;
}

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.

🔍 Zero-limit queues change health label from "At capacity" to "Backlogged"

isQueueAtCapacity now short-circuits on limit <= 0, whereas the inlined predicate it replaces (running >= limit && queued > 0) reported a zero-limit queue with a backlog as "At capacity". A queue whose concurrency limit is explicitly 0 (paused-by-limit) will now show "Backlogged" in the queues list and will no longer emit a concurrency_saturation signal to the agent. The new semantics are arguably more accurate, but it is a user-visible label change in the queues table that isn't called out in the PR description or the server-changes note.

Open in Devin Review

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

Sign up for freeto 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

@kathiekiwi