Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
449e9f3
feat(webapp): the dashboard agent's UI
kathiekiwi Aug 7, 2026
44cf389
style(webapp): use the agent's own logo in the Help & Feedback entry
kathiekiwi Aug 7, 2026
1d094a8
fix(webapp): drop Keep digging once the turn has already kept digging
kathiekiwi Aug 7, 2026
ebd1b76
feat(webapp): give Cmd-I and the CLI's help link back to Ask AI
kathiekiwi Aug 8, 2026
6120967
test(webapp): give the untrustworthy report fixture the reason field …
kathiekiwi Aug 8, 2026
a447e86
fix(webapp): give each copyable task snippet its own SDK import
kathiekiwi Aug 8, 2026
9acbcbf
fix(webapp): drop a run diagnosis action that has nowhere to go
kathiekiwi Aug 8, 2026
4db8ab3
fix(webapp): stop Cmd-J opening Chrome's downloads
kathiekiwi Aug 8, 2026
e73112d
fix(webapp): keep an empty chat open instead of showing a new one
kathiekiwi Aug 8, 2026
4e27872
docs: say where the docs went now the header button is gone
kathiekiwi Aug 8, 2026
8c682da
perf(webapp): keep report blocks out of the investigation winner pass
kathiekiwi Aug 8, 2026
db578e0
fix(webapp): give icon-only tooltip buttons an accessible name
kathiekiwi Aug 8, 2026
4472339
fix(webapp): stop a zero-limit queue reading as at capacity
kathiekiwi Aug 8, 2026
466ed7d
fix(webapp): stop the agent's retry duplicating the failed message
kathiekiwi Aug 8, 2026
2c7575f
docs(webapp): say who Cmd-I actually opens the agent for
kathiekiwi Aug 8, 2026
e8c43e4
fix(webapp): time the report sparkline's bars from the report, not th…
kathiekiwi Aug 8, 2026
a09d8a8
fix(webapp): key a repeated view block by its own position
kathiekiwi Aug 8, 2026
e27e80e
fix(webapp): send a deep-linked question once
kathiekiwi Aug 8, 2026
084d45f
fix(webapp): stop the URI resolver working after the panel closes
kathiekiwi Aug 8, 2026
022388c
fix(webapp): announce the composer's counter and name the history button
kathiekiwi Aug 8, 2026
5e8b6e1
fix(webapp): keep the hero's heading reachable when it outgrows the p…
kathiekiwi Aug 8, 2026
8d2e2c9
fix(webapp): pin the AI-help redirect to the dashboard's own origin
kathiekiwi Aug 8, 2026
fd11d3a
fix(webapp): read the agent's page label from the left of the path
kathiekiwi Aug 8, 2026
aaed07b
fix(webapp): step the composer's live region instead of reading every…
kathiekiwi Aug 8, 2026
b282c79
fix(webapp): key an investigation's action rows by position, not by kind
kathiekiwi Aug 8, 2026
886e802
fix(webapp): treat a transcript re-read that isn't a list as a failed…
kathiekiwi Aug 8, 2026
f64c1d1
fix(webapp): stop the Ask AI host remounting the dashboard, and four …
kathiekiwi Aug 9, 2026
43a9199
fix(webapp): stop the CLI's AI help link dead-ending without an AI su…
kathiekiwi Aug 9, 2026
d87ca1d
perf(webapp): restore the stripped-message cache the UI rewrite dropped
kathiekiwi Aug 9, 2026
57f0762
test(webapp): use a static import in the sweep card test
kathiekiwi Aug 9, 2026
0787f0c
Merge remote-tracking branch 'origin/feat/dashboard-agent-flows' into…
kathiekiwi Aug 9, 2026
c78eaca
Merge remote-tracking branch 'origin/feat/dashboard-agent-flows' into…
kathiekiwi Aug 9, 2026
a72b797
Merge remote-tracking branch 'origin/feat/dashboard-agent-flows' into…
kathiekiwi Aug 9, 2026
a6782c0
Merge remote-tracking branch 'origin/feat/dashboard-agent-flows' into…
kathiekiwi Aug 9, 2026
edb48d5
fix(webapp): close the queued-reload race and the backslash route escape
kathiekiwi Aug 9, 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.
78 changes: 31 additions & 47 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 @@ -17,13 +17,17 @@ import { useSearchParams } from "@remix-run/react";
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 { useCallback, useEffect, useRef, useState } from "react";
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 { useAskAiHost } from "./dashboard-agent/askAiOpenRequest";
import { Button } from "./primitives/Buttons";
import { Callout } from "./primitives/Callout";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./primitives/Dialog";
Expand All @@ -40,11 +44,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 +66,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,45 +82,30 @@ 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). It renders no page content and wraps
* nothing: entry points reach it through `requestAskAi`, so the Kapa provider mounting after
* hydration can never remount the app around it.
*/
export function AskAIRoot({
children,
}: {
children: (openAskAI: (() => void) | undefined) => ReactNode;
}) {
const { isManagedCloud } = useFeatures();
const websiteId = useKapaWebsiteId();
export function AskAIRoot() {
const availability = useAskAiAvailability();

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

return (
<ClientOnly fallback={<>{children(undefined)}</>}>
{() => <AskAIRootProvider websiteId={websiteId}>{children}</AskAIRootProvider>}
</ClientOnly>
);
const websiteId = availability.kapaWebsiteId!;

return <ClientOnly>{() => <AskAIRootProvider websiteId={websiteId} />}</ClientOnly>;
}

function AskAIRootProvider({
websiteId,
children,
}: {
websiteId: string;
children: (openAskAI: () => void) => ReactNode;
}) {
function AskAIRootProvider({ websiteId }: { websiteId: string }) {
const { isOpen, setIsOpen, initialQuery, openAskAI, closeAskAI } = useAskAIState();

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

return (
<KapaProvider
Expand All @@ -134,7 +118,6 @@ function AskAIRootProvider({
}}
botProtectionMechanism="hcaptcha"
>
{children(() => openAskAI())}
<AskAIDialog
initialQuery={initialQuery}
isOpen={isOpen}
Expand All @@ -145,15 +128,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
105 changes: 58 additions & 47 deletions apps/webapp/app/components/BlankStatePanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
v3NewProjectAlertPath,
v3NewSchedulePath,
} from "~/utils/pathBuilder";
import { AskAI } from "./AskAI";
import { AskAgentButton } from "./dashboard-agent/AskAgentButton";
import { CodeBlock } from "./code/CodeBlock";
import { InlineCode } from "./code/InlineCode";
import { environmentFullTitle, EnvironmentIcon } from "./environments/EnvironmentLabel";
Expand Down Expand Up @@ -62,6 +62,55 @@ import {
import { StepContentContainer } from "./StepContentContainer";
import { V4Badge } from "./V4Badge";

/**
* What the agent is asked when it's opened from a deployment setup panel. The panel is the docs
* answer; the agent is for the part the docs can't answer — this project, this environment.
*/
const ASK_AGENT_DEPLOY_PROMPT =
"I'm trying to deploy my tasks to this environment. Walk me through it and tell me if anything about this project or environment is going to get in the way.";

/** The docs links the deployment panels offer to anyone without the agent. */
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 +319,10 @@ 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 />
{/* One entry point instead of two: the docs links were a guess at which page you
needed, and the agent can look at this project and answer for it. Someone with no
agent still gets the links. */}
<AskAgentButton prompt={ASK_AGENT_DEPLOY_PROMPT} fallback={<DeployDocsLinks />} />
</div>
</div>
<StepNumber stepNumber="→" title="Switch to a deployed environment" />
Expand Down Expand Up @@ -658,29 +688,10 @@ 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 />
{/* One entry point instead of two: the docs links were a guess at which page you
needed, and the agent can look at this project and answer for it. Someone with no
agent still gets the links. */}
<AskAgentButton prompt={ASK_AGENT_DEPLOY_PROMPT} fallback={<DeployDocsLinks />} />
</div>
</div>
<ClientTabs defaultValue="github">
Expand Down
54 changes: 50 additions & 4 deletions apps/webapp/app/components/Shortcuts.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { KeyboardIcon } from "~/assets/icons/KeyboardIcon";
import { useState } from "react";
import { ASK_AGENT_LABEL } from "~/components/dashboard-agent/agent-identity";
import { type AiShortcutRow, aiShortcutRows } from "~/components/dashboard-agent/ai-entry-points";
import { ASK_AI_SHORTCUT, askAiCanOpen } from "~/components/dashboard-agent/ask-ai-channels";
import { useDashboardAgentAvailable } from "~/components/dashboard-agent/dashboardAgentOpenRequest";
import { NEW_CHAT_SHORTCUT } from "~/components/dashboard-agent/DashboardAgentHeader";
import { TOGGLE_PANEL_SHORTCUT } from "~/components/dashboard-agent/dashboardAgentLauncher";
import { useAskAiAvailability } from "~/hooks/useAskAiAvailability";
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
import { Header3 } from "./primitives/Headers";
import { SideMenuItemButton } from "./navigation/SideMenuItem";
Expand Down Expand Up @@ -41,6 +48,11 @@ export function ShortcutsAutoOpen() {
}

function ShortcutContent() {
const agent = useDashboardAgentAvailable();
const askAi = askAiCanOpen(useAskAiAvailability());
const rows = aiShortcutRows({ agent, askAi });
const shows = (row: AiShortcutRow) => rows.includes(row);

return (
<SheetContent>
<SheetHeader>
Expand All @@ -62,10 +74,27 @@ function ShortcutContent() {
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
</Shortcut>
<Shortcut name="Ask AI">
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "i" }} variant="medium/bright" />
</Shortcut>
{shows("agent-toggle") && (
<Shortcut name={ASK_AGENT_LABEL}>
<ShortcutKey
shortcut={{ modifiers: TOGGLE_PANEL_SHORTCUT.modifiers }}
variant="medium/bright"
/>
<ShortcutKey
shortcut={{ key: TOGGLE_PANEL_SHORTCUT.key }}
variant="medium/bright"
/>
</Shortcut>
)}
{shows("ask-ai") && (
<Shortcut name="Ask AI">
<ShortcutKey
shortcut={{ modifiers: ASK_AI_SHORTCUT.modifiers }}
variant="medium/bright"
/>
<ShortcutKey shortcut={{ key: ASK_AI_SHORTCUT.key }} variant="medium/bright" />
</Shortcut>
)}
<Shortcut name="Filter">
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
</Shortcut>
Expand Down Expand Up @@ -94,6 +123,23 @@ function ShortcutContent() {
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
</Shortcut>
</div>
{shows("agent-new-chat") && (
<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>
{shows("agent-close-chat") && (
<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