Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
43311df
feat(webapp): the dashboard agent's UI
kathiekiwi Aug 7, 2026
dc1104c
style(webapp): use the agent's own logo in the Help & Feedback entry
kathiekiwi Aug 7, 2026
815aa1d
fix(webapp): drop Keep digging once the turn has already kept digging
kathiekiwi Aug 7, 2026
5993b47
feat(webapp): give Cmd-I and the CLI's help link back to Ask AI
kathiekiwi Aug 8, 2026
189ce97
test(webapp): give the untrustworthy report fixture the reason field …
kathiekiwi Aug 8, 2026
4222e3f
fix(webapp): give each copyable task snippet its own SDK import
kathiekiwi Aug 8, 2026
5b1f482
fix(webapp): drop a run diagnosis action that has nowhere to go
kathiekiwi Aug 8, 2026
2874993
fix(webapp): stop Cmd-J opening Chrome's downloads
kathiekiwi Aug 8, 2026
a9788c4
fix(webapp): keep an empty chat open instead of showing a new one
kathiekiwi Aug 8, 2026
fa8d41b
docs: say where the docs went now the header button is gone
kathiekiwi Aug 8, 2026
1c22c50
perf(webapp): keep report blocks out of the investigation winner pass
kathiekiwi Aug 8, 2026
246c643
fix(webapp): give icon-only tooltip buttons an accessible name
kathiekiwi Aug 8, 2026
6750446
fix(webapp): stop a zero-limit queue reading as at capacity
kathiekiwi Aug 8, 2026
8bc587d
fix(webapp): stop the agent's retry duplicating the failed message
kathiekiwi Aug 8, 2026
cd953f6
docs(webapp): say who Cmd-I actually opens the agent for
kathiekiwi Aug 8, 2026
7630cd1
fix(webapp): time the report sparkline's bars from the report, not th…
kathiekiwi Aug 8, 2026
6dcfd38
fix(webapp): key a repeated view block by its own position
kathiekiwi Aug 8, 2026
ac53d0f
fix(webapp): send a deep-linked question once
kathiekiwi Aug 8, 2026
622d3d8
fix(webapp): stop the URI resolver working after the panel closes
kathiekiwi Aug 8, 2026
9bb1f52
fix(webapp): announce the composer's counter and name the history button
kathiekiwi Aug 8, 2026
f31e283
fix(webapp): keep the hero's heading reachable when it outgrows the p…
kathiekiwi Aug 8, 2026
17a0f07
fix(webapp): pin the AI-help redirect to the dashboard's own origin
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 @@ -11,4 +11,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" />
</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
31 changes: 31 additions & 0 deletions apps/webapp/app/components/dashboard-agent/ActionsBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type {
ActionsBlock as ActionsBlockPayload,
AgentIntent,
} from "@internal/dashboard-agent-contracts";
import { Button } from "~/components/primitives/Buttons";
import { ChatActionsRow } from "./chat-layout";
import { renderableActions } from "./view-actions";

export function ActionsBlock({
block,
onIntent,
}: {
block: ActionsBlockPayload;
onIntent?: (intent: AgentIntent) => void;
}) {
const renderable = renderableActions(block.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>
);
}
Loading
Loading