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

@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: 83ae7f2

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: c57627a0-d0b4-41ea-bb6c-f04e63e4c96d

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 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[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-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[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

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

This comment was marked as resolved.

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

This comment was marked as resolved.

@kathiekiwi
kathiekiwi force-pushed the feat/dashboard-agent-ui branch 2 times, most recently from 2460d14 to 9447128 Compare August 8, 2026 18:08

@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 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-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 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 thread apps/webapp/app/components/queues/queue-thresholds.ts
@kathiekiwi
kathiekiwi force-pushed the feat/dashboard-agent-ui branch from ddb9f3c to 88efcf7 Compare August 9, 2026 00:01
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Observability map

As of 83ae7f2.

Nothing in this pull request moves the report any more. The findings an earlier push reported are gone.

The score and findings here are report-only and never gate the merge. Separately, a required test suite keeps this tool's symbol and route lists in sync with the code they name, and can fail a pull request that renames or removes a symbol they reference, or that adds the first route with a segment they anticipate. Each failure names the list to edit. The rules and their reasons: internal-packages/observability-map/README.md.

@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 on lines +68 to +70
<Shortcut name={ASK_AGENT_LABEL}>
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "i" }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: TOGGLE_PANEL_SHORTCUT.key }} variant="medium/bright" />

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.

🟡 Keyboard shortcut list advertises an AI shortcut that most users cannot use, and drops the one that works

The keyboard shortcuts panel now lists the AI shortcut as Cmd/Ctrl-J (TOGGLE_PANEL_SHORTCUT.key at apps/webapp/app/components/Shortcuts.tsx:67) even though that key only works for the small set of users the dashboard agent is enabled for, and the Cmd/Ctrl-I shortcut that still works for everyone else is no longer listed anywhere.

Impact: Users without the new agent see a documented shortcut that does nothing, and no longer see the AI shortcut that does work.

Where the shortcut is registered vs. where it is documented

TOGGLE_PANEL_SHORTCUT (⌘J) is registered only inside DashboardAgent and is disabled when hasAccess is false (apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx:77-88). hasAccess comes from canAccessDashboardAgent in the environment layout loader, so it is off for most users.

ASK_AI_SHORTCUT (⌘I) is still registered app-wide by AskAIRootProvider (apps/webapp/app/components/AskAI.tsx:118-121) whenever Ask AI can open, but Shortcuts.tsx no longer lists it.

Separately, the new "Chat" group lists "New chat" with NEW_CHAT_SHORTCUT (apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx:30-34), which is the same ⌘J, so the panel shows two different shortcut names bound to an identical keystroke with no indication that the behaviour is contextual.

Prompt for agents
apps/webapp/app/components/Shortcuts.tsx unconditionally lists the dashboard agent's ⌘J shortcut under ASK_AGENT_LABEL, plus a "Chat" group whose "New chat" entry is the same ⌘J. The agent shortcut is only registered when the environment layout resolves hasAccess=true (see DashboardAgent.tsx), so for users without agent access these rows document keystrokes that do nothing. Meanwhile ⌘I (ASK_AI_SHORTCUT), which is still registered app-wide by AskAIRootProvider whenever Ask AI is available, is no longer listed at all.

Consider gating the agent rows on the same availability signal the other entry points use (useDashboardAgentAvailable from dashboardAgentOpenRequest), and listing ⌘I when Ask AI can open, so the panel only advertises shortcuts that actually fire. Also clarify that ⌘J is contextual (open panel vs. new chat) rather than showing it as two independent shortcuts.
Open in Devin Review

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

Comment on lines +130 to +143
{agentAvailable && (
<div className="flex flex-col gap-1 p-1">
<SideMenuItemButton
icon={<AgentMonoLogo size={18} decorative />}
name={ASK_AGENT_LABEL}
data-action="ask-agent"
trailing={<ShortcutKey shortcut={TOGGLE_PANEL_SHORTCUT} variant="medium" />}
onClick={() => {
setHelpMenuOpen(false);
requestDashboardAgent();
}}
/>
</div>
)}

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.

🔍 Ask AI entry point disappears entirely for users without dashboard-agent access

The Help & Feedback popover's "Ask AI" item was replaced by an "Ask Trigger" item gated on useDashboardAgentAvailable(), which is only true when a DashboardAgent host is mounted with hasAccess=true. On managed cloud, users who do not have dashboard-agent access previously had a discoverable "Ask AI" entry here; now the popover has no AI entry at all, even though ⌘I still opens the Kapa dialog (registered app-wide by AskAIRootProvider at apps/webapp/app/components/AskAI.tsx:118-121). Combined with the removal of every page-header Docs button, the only remaining path for those users is the undiscoverable ⌘I. Worth confirming that's intended for the rollout window while the agent flag is off for most orgs.

Open in Devin Review

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

Comment on lines +8 to +24
export function AskAgentButton({
prompt,
label = ASK_AGENT_LABEL,
iconOnly = false,
variant = "small-menu-item",
className,
fallback = null,
}: {
prompt?: string;
label?: string;
iconOnly?: boolean;
variant?: "small-menu-item" | "secondary/small" | "primary/small";
className?: string;
fallback?: React.ReactNode;
}) {
const available = useDashboardAgentAvailable();
if (!available) return <>{fallback}</>;

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.

🔍 AskAgentButton is added but never mounted anywhere

AskAgentButton is a new component with tooltip/icon-only/fallback handling, but a repo-wide grep finds no importers — the only reference is its own definition. Either an intended call site was dropped from this PR or it is intended for a later stack; as it stands it is dead code that will be shipped and needs to stay in sync with dashboardAgentOpenRequest for no benefit.

Open in Devin Review

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

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
kathiekiwi force-pushed the feat/dashboard-agent-ui branch from 88efcf7 to 83ae7f2 Compare August 9, 2026 08:20

@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 4 new potential issues.

Open in Devin Review

Comment on lines +130 to +143
{agentAvailable && (
<div className="flex flex-col gap-1 p-1">
<SideMenuItemButton
icon={<AgentMonoLogo size={18} decorative />}
name={ASK_AGENT_LABEL}
data-action="ask-agent"
trailing={<ShortcutKey shortcut={TOGGLE_PANEL_SHORTCUT} variant="medium" />}
onClick={() => {
setHelpMenuOpen(false);
requestDashboardAgent();
}}
/>
</div>
)}

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.

🟡 Cloud users without the new assistant lose the only visible AI help entry in the sidebar

The sidebar help menu's AI entry is now shown only when the new assistant is available (agentAvailable && at apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx:130), so accounts that don't have it see no AI option at all, even though the old assistant still works behind a keyboard shortcut.

Impact: Users who are not on the new assistant lose the clickable "Ask AI" help entry and are left with an undiscoverable keyboard shortcut.

What changed and who it affects

Previously the popover wrapped its contents in AskAIRoot and rendered an "Ask AI" item whenever openAskAI was defined — i.e. on managed cloud with a Kapa website id, regardless of the dashboard-agent gate. Now the item is replaced by an "Ask Trigger" item gated on useDashboardAgentAvailable(), which is only true when the agent host is registered (hasAccess from canAccessDashboardAgent).

So for managed-cloud users without the agent flag, the popover has no AI entry. Ask AI itself is still reachable — ⌘I is registered by AskAIRoot in apps/webapp/app/routes/_app/route.tsx — but nothing in the UI advertises it, and Shortcuts.tsx now labels the ⌘J row "Ask Trigger" and no longer lists ⌘I at all (apps/webapp/app/components/Shortcuts.tsx:68-70). This is a user-visible change for the flag-off population, contrary to the PR's "no behavior change with the flag off".

Prompt for agents
In `apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx` the AI entry is now conditional on `useDashboardAgentAvailable()`. Users on managed cloud who do not have dashboard-agent access previously saw an "Ask AI" item here and now see nothing, while ⌘I (registered by `AskAIRoot` in `app/routes/_app/route.tsx`) still opens Kapa. Decide whether these users should keep a visible Ask AI entry: one option is to render the legacy Ask AI item as a fallback when the agent host is not mounted but `askAiCanOpen(useAskAiAvailability())` is true, reusing the open-function the `_app`-level `AskAIRoot` already provides (which would need to be exposed via context rather than only the render prop). Also consider keeping the ⌘I row in `Shortcuts.tsx` for those users.
Open in Devin Review

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

Comment on lines +19 to +22
vi.mock("~/services/dashboardAgentDb.server", () => ({ dashboardAgentDb: undefined }));

const { sweepDashboardAgentInvestigations } =
await import("~/services/dashboardAgentInvestigationSweep.server");

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.

🟡 New test mocks an application module instead of using a real container, against the repository's testing rule

The new sweep test replaces a real module with a fake one (vi.mock("~/services/dashboardAgentDb.server", …) at apps/webapp/test/dashboardAgentInvestigationSweepCard.test.ts:19), which the repository's testing rules forbid.

Impact: The test suite diverges from the project's stated testing approach, so the guidance stops being reliable for future contributors.

Rules involved

AGENTS.md, "Testing": "We use vitest exclusively. Never mock anything - use testcontainers instead." The test mocks ~/services/dashboardAgentDb.server to avoid opening a pool; the repo's @internal/testcontainers helpers (postgresTest, containerTest) are the sanctioned way to do this.

The mock also forces a dynamic import (const { sweepDashboardAgentInvestigations } = await import("~/services/dashboardAgentInvestigationSweep.server"), lines 21-22), which AGENTS.md's "Imports" section asks to avoid in favour of static imports.

Prompt for agents
`apps/webapp/test/dashboardAgentInvestigationSweepCard.test.ts` uses `vi.mock` on `~/services/dashboardAgentDb.server` purely so that importing `dashboardAgentInvestigationSweep.server` does not open a connection pool, and then has to use `await import(...)` because of the mock ordering. AGENTS.md forbids mocking (use `@internal/testcontainers` instead) and prefers static imports. Since every write in this test is already injected through the `listStale`/`settleAndClose` dependencies, the cleanest fix is to move the pure, dependency-injected `sweepDashboardAgentInvestigations` function into a module that does not import the datastore singleton at all, so the test can import it statically with no mock. Alternatively, run the test against a real Postgres via `postgresTest`/`containerTest`.
Open in Devin Review

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

Comment on lines +45 to 56
return redirect(
aiHelpRedirectUrl({
environmentPath: v3EnvironmentPath(
{ slug: project.organization.slug },
{ slug: project.slug },
{ slug: "dev" }
),
origin: env.LOGIN_ORIGIN,
query,
})
);
// The `ask` param is picked up in the environment layout (`useDashboardAgentOpenRequests`).
newUrl.searchParams.set("ask", query);

return redirect(newUrl.toString());
}

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.

🔍 The CLI's ai-help link now dead-ends for self-hosted users without agent access

The redirect switched from ?ask= (read by the agent) to ?aiHelp= (read by Ask AI). agentDeepLinkParams only lets the agent claim aiHelp when Kapa cannot open, and the agent's reader is additionally gated on hasAccess. So on a self-hosted install — exactly where trigger dev's "Get a fix for this error using AI" link is most likely followed and where Kapa never loads — nobody consumes the param unless the user also has dashboard-agent access: the user lands on the dev environment page with a stray ?aiHelp=<error> in the URL and no assistant opens. Worth deciding whether the CLI should suppress the link, or the route should refuse to redirect, when neither surface can answer.

Open in Devin Review

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

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