Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
015e34b
feat(webapp): the dashboard agent's UI
kathiekiwi Aug 7, 2026
42ca86c
style(webapp): use the agent's own logo in the Help & Feedback entry
kathiekiwi Aug 7, 2026
a4ab30c
fix(webapp): drop Keep digging once the turn has already kept digging
kathiekiwi Aug 7, 2026
7d57c9b
feat(webapp): give Cmd-I and the CLI's help link back to Ask AI
kathiekiwi Aug 8, 2026
e375ae1
test(webapp): give the untrustworthy report fixture the reason field …
kathiekiwi Aug 8, 2026
ad6827a
fix(webapp): give each copyable task snippet its own SDK import
kathiekiwi Aug 8, 2026
b8610d1
fix(webapp): drop a run diagnosis action that has nowhere to go
kathiekiwi Aug 8, 2026
dcdc892
fix(webapp): stop Cmd-J opening Chrome's downloads
kathiekiwi Aug 8, 2026
e35b239
fix(webapp): keep an empty chat open instead of showing a new one
kathiekiwi Aug 8, 2026
507faeb
docs: say where the docs went now the header button is gone
kathiekiwi Aug 8, 2026
fe770c0
perf(webapp): keep report blocks out of the investigation winner pass
kathiekiwi Aug 8, 2026
ad86281
fix(webapp): give icon-only tooltip buttons an accessible name
kathiekiwi Aug 8, 2026
06ab219
fix(webapp): stop a zero-limit queue reading as at capacity
kathiekiwi Aug 8, 2026
43f7a2f
fix(webapp): stop the agent's retry duplicating the failed message
kathiekiwi Aug 8, 2026
838e385
docs(webapp): say who Cmd-I actually opens the agent for
kathiekiwi Aug 8, 2026
151e20b
fix(webapp): time the report sparkline's bars from the report, not th…
kathiekiwi Aug 8, 2026
fa6536f
fix(webapp): key a repeated view block by its own position
kathiekiwi Aug 8, 2026
7fbb2e1
fix(webapp): send a deep-linked question once
kathiekiwi Aug 8, 2026
7b87e59
fix(webapp): stop the URI resolver working after the panel closes
kathiekiwi Aug 8, 2026
f6a22fc
fix(webapp): announce the composer's counter and name the history button
kathiekiwi Aug 8, 2026
7aa8ac3
fix(webapp): keep the hero's heading reachable when it outgrows the p…
kathiekiwi Aug 8, 2026
3c41573
fix(webapp): pin the AI-help redirect to the dashboard's own origin
kathiekiwi Aug 8, 2026
5655e1d
fix(webapp): read the agent's page label from the left of the path
kathiekiwi Aug 8, 2026
50b7801
fix(webapp): step the composer's live region instead of reading every…
kathiekiwi Aug 8, 2026
c2f7c6f
fix(webapp): key an investigation's action rows by position, not by kind
kathiekiwi Aug 8, 2026
00a22c6
fix(webapp): treat a transcript re-read that isn't a list as a failed…
kathiekiwi Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .server-changes/dashboard-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ The health report reads the same everywhere — dashboard, terminal, editor. A v

A sample of conversations is scored automatically so the agent keeps getting better. Only the score and a one-line summary are kept, never your messages, data or code, and we can switch it off for your organization on request.

Separately: a queue's wait times, peak depth, throughput and throttling can now be read from the API, and the Docs button has been removed from page headers.
The Docs button is gone from page headers — ask the agent instead, or open Documentation from Help & Feedback.

Separately: a queue's wait times, peak depth, throughput and throttling can now be read from the API.
52 changes: 25 additions & 27 deletions apps/webapp/app/components/AskAI.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @deprecated Superseded by the dashboard agent (`components/dashboard-agent`). Nothing mounts
* this any more — every Ask AI entry point now opens Ask Trigger. Kept until the agent has
* shipped, then removed along with `@kapaai/react-sdk` and `KAPA_AI_WEBSITE_ID`.
* Mostly superseded by the dashboard agent (`components/dashboard-agent`), which owns every
* entry point except two: ⌘I and the CLI's `?aiHelp=` link still open Ask AI. `AskAIRoot` is
* mounted by the `_app` layout for those; the `AskAI` button below is mounted nowhere.
*/

import {
Expand All @@ -18,12 +18,15 @@ import DOMPurify from "dompurify";
import { motion } from "framer-motion";
import { marked } from "marked";
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
import { useTypedRouteLoaderData } from "remix-typedjson";
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
import { SparkleListIcon } from "~/assets/icons/SparkleListIcon";
import { useFeatures } from "~/hooks/useFeatures";
import { useAskAiAvailability } from "~/hooks/useAskAiAvailability";
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
import { type loader } from "~/root";
import {
ASK_AI_DEEP_LINK_PARAM,
ASK_AI_SHORTCUT,
askAiCanOpen,
} from "./dashboard-agent/ask-ai-channels";
import { Button } from "./primitives/Buttons";
import { Callout } from "./primitives/Callout";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./primitives/Dialog";
Expand All @@ -40,11 +43,6 @@ import {
} from "./primitives/Tooltip";
import { ClientOnly } from "remix-utils/client-only";

function useKapaWebsiteId() {
const routeMatch = useTypedRouteLoaderData<typeof loader>("root");
return routeMatch?.kapa.websiteId;
}

/** Open/close state for the Ask AI dialog, including the `?aiHelp=` deep-link handling. */
function useAskAIState() {
const [isOpen, setIsOpen] = useState(false);
Expand All @@ -67,14 +65,14 @@ function useAskAIState() {

// Handle URL param functionality
useEffect(() => {
const aiHelp = searchParams.get("aiHelp");
const aiHelp = searchParams.get(ASK_AI_DEEP_LINK_PARAM);
if (aiHelp) {
// Delay to avoid hCaptcha bot detection
window.setTimeout(() => openAskAI(aiHelp), 1000);

// Clone instead of mutating in place
const next = new URLSearchParams(searchParams);
next.delete("aiHelp");
next.delete(ASK_AI_DEEP_LINK_PARAM);
setSearchParams(next);
}
}, [searchParams, openAskAI]);
Expand All @@ -83,25 +81,24 @@ function useAskAIState() {
}

/**
* Hosts Ask AI (Kapa provider, ⌘I shortcut, dialog) for a menu that renders its own trigger. Wrap
* it around the popover, not inside, so the dialog and shortcut survive the popover closing.
* `children` receives the open function, or undefined when Ask AI is unavailable (self-hosted, no
* Kapa website id, or SSR).
*
* @deprecated See the note at the top of this file.
* Hosts Ask AI (Kapa provider, ⌘I shortcut, dialog) around a subtree that renders its own
* triggers. Wrap it around them, never inside, so the dialog and the shortcut survive whatever
* opened them closing. `children` receives the open function, or undefined when Ask AI is
* unavailable (self-hosted, no Kapa website id, or SSR).
*/
export function AskAIRoot({
children,
}: {
children: (openAskAI: (() => void) | undefined) => ReactNode;
}) {
const { isManagedCloud } = useFeatures();
const websiteId = useKapaWebsiteId();
const availability = useAskAiAvailability();

if (!isManagedCloud || !websiteId) {
if (!askAiCanOpen(availability)) {
return <>{children(undefined)}</>;
}

const websiteId = availability.kapaWebsiteId!;

return (
<ClientOnly fallback={<>{children(undefined)}</>}>
{() => <AskAIRootProvider websiteId={websiteId}>{children}</AskAIRootProvider>}
Expand All @@ -119,7 +116,7 @@ function AskAIRootProvider({
const { isOpen, setIsOpen, initialQuery, openAskAI, closeAskAI } = useAskAIState();

useShortcutKeys({
shortcut: { modifiers: ["mod"], key: "i", enabledOnInputElements: true },
shortcut: ASK_AI_SHORTCUT,
action: () => openAskAI(),
});

Expand All @@ -145,15 +142,16 @@ function AskAIRootProvider({
);
}

/** @deprecated See the note at the top of this file. */
/** @deprecated Mounted nowhere: the sidebar's AI entry point is the dashboard agent. */
export function AskAI({ isCollapsed = false }: { isCollapsed?: boolean }) {
const { isManagedCloud } = useFeatures();
const websiteId = useKapaWebsiteId();
const availability = useAskAiAvailability();

if (!isManagedCloud || !websiteId) {
if (!askAiCanOpen(availability)) {
return null;
}

const websiteId = availability.kapaWebsiteId!;

return (
<ClientOnly
fallback={
Expand Down
90 changes: 43 additions & 47 deletions apps/webapp/app/components/BlankStatePanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
v3NewProjectAlertPath,
v3NewSchedulePath,
} from "~/utils/pathBuilder";
import { AskAI } from "./AskAI";
import { CodeBlock } from "./code/CodeBlock";
import { InlineCode } from "./code/InlineCode";
import { environmentFullTitle, EnvironmentIcon } from "./environments/EnvironmentLabel";
Expand Down Expand Up @@ -62,6 +61,47 @@ import {
import { StepContentContainer } from "./StepContentContainer";
import { V4Badge } from "./V4Badge";

function DeployDocsLinks() {
return (
<>
<SimpleTooltip
asChild
tabbable
button={
// Span wrapper: LinkButton drops the pointer-event props Radix injects via asChild, so
// the tooltip trigger has to be a plain element (same pattern as FavoritePageButton).
<span className="flex">
<LinkButton
variant="small-menu-item"
LeadingIcon={BookOpenIcon}
leadingIconClassName="text-blue-500"
to={docsPath("deployment/overview")}
aria-label="Deploy docs"
/>
</span>
}
content="Deploy docs"
/>
<SimpleTooltip
asChild
tabbable
button={
<span className="flex">
<LinkButton
variant="small-menu-item"
LeadingIcon={QuestionMarkCircleIcon}
leadingIconClassName="text-blue-500"
to={docsPath("troubleshooting#deployment")}
aria-label="Troubleshooting docs"
/>
</span>
}
content="Troubleshooting docs"
/>
</>
);
}

export function HasNoTasksDev() {
return (
<PackageManagerProvider>
Expand Down Expand Up @@ -270,29 +310,7 @@ export function DeploymentsNoneDev() {
<Header1>Deploy your tasks</Header1>
</div>
<div className="flex items-center">
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={BookOpenIcon}
leadingIconClassName="text-blue-500"
to={docsPath("deployment/overview")}
/>
}
content="Deploy docs"
/>
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={QuestionMarkCircleIcon}
leadingIconClassName="text-blue-500"
to={docsPath("troubleshooting#deployment")}
/>
}
content="Troubleshooting docs"
/>
<AskAI />
<DeployDocsLinks />
</div>
</div>
<StepNumber stepNumber="→" title="Switch to a deployed environment" />
Expand Down Expand Up @@ -658,29 +676,7 @@ function DeploymentOnboardingSteps() {
</Header1>
</div>
<div className="flex items-center">
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={BookOpenIcon}
leadingIconClassName="text-blue-500"
to={docsPath("deployment/overview")}
/>
}
content="Deploy docs"
/>
<SimpleTooltip
button={
<LinkButton
variant="small-menu-item"
LeadingIcon={QuestionMarkCircleIcon}
leadingIconClassName="text-blue-500"
to={docsPath("troubleshooting#deployment")}
/>
}
content="Troubleshooting docs"
/>
<AskAI />
<DeployDocsLinks />
</div>
</div>
<ClientTabs defaultValue="github">
Expand Down
20 changes: 18 additions & 2 deletions apps/webapp/app/components/Shortcuts.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { KeyboardIcon } from "~/assets/icons/KeyboardIcon";
import { useState } from "react";
import { ASK_AGENT_LABEL } from "~/components/dashboard-agent/agent-identity";
import { NEW_CHAT_SHORTCUT } from "~/components/dashboard-agent/DashboardAgentHeader";
import { TOGGLE_PANEL_SHORTCUT } from "~/components/dashboard-agent/dashboardAgentLauncher";
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
import { Header3 } from "./primitives/Headers";
import { SideMenuItemButton } from "./navigation/SideMenuItem";
Expand Down Expand Up @@ -62,9 +65,9 @@ function ShortcutContent() {
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
</Shortcut>
<Shortcut name="Ask AI">
<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" />
Comment on lines +68 to +70

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.

</Shortcut>
<Shortcut name="Filter">
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
Expand Down Expand Up @@ -94,6 +97,19 @@ function ShortcutContent() {
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
</Shortcut>
</div>
<div className="space-y-3">
<Header3>Chat</Header3>
<Shortcut name="New chat">
<ShortcutKey
shortcut={{ modifiers: NEW_CHAT_SHORTCUT.modifiers }}
variant="medium/bright"
/>
<ShortcutKey shortcut={{ key: NEW_CHAT_SHORTCUT.key }} variant="medium/bright" />
</Shortcut>
<Shortcut name="Close chat">
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
</Shortcut>
</div>
<div className="space-y-3">
<Header3>Runs page</Header3>
<Shortcut name="Bulk action: Cancel runs">
Expand Down
Loading
Loading