Skip to content

Commit 9a3bee0

Browse files
authored
feat(webapp): dashboard agent — UI (#4529)
Stacked on #4418. Merge that first. The UI slice of the dashboard agent: the side panel, the chat transport wiring, message and card rendering, suggested prompts, and chat history. #4418 works without this — the system is simply invisible. The diff is mostly components, so the notes below cover only the three decisions you can't read off the markup. Behavior and a hands-on walkthrough live in GUIDEBOOK.md, which lands with #4525. ## Decisions worth knowing - **Action rows always render at the end of a turn.** The model's emission order isn't trusted for layout, so action blocks are split out of the stream and appended last. Display only — `answered` stays keyed on the emission index. - **The last-chat memory is org-true.** It's keyed by the chat's own organization, and a foreign or deleted chat comes back as a 404 the client treats as gone, rather than an empty chat it keeps around. - **A dead stream self-heals from the settled transcript.** Terminal records are written to the chat row after the client's stream closes, so the panel re-reads it. The poll gate is any unfinished turn — a dangling tool part, not just an open investigation. ## Notes - Gated by `canAccessDashboardAgent`; no behavior change with the flag off. - Page marks: `handle.agentPageContext` on 47 routes, ~20 lines each. - Entry points: Ask Trigger button, ⌘J, Help & Feedback. The old ⌘I and `?aiHelp=` links keep working. ## Screenshots <img width="1440" height="788" alt="Screenshot 2026-08-07 at 15 14 29" src="https://github.com/user-attachments/assets/f4e89e8d-13ed-4be3-a88d-d5cca3ece0fa" />
1 parent 4569657 commit 9a3bee0

204 files changed

Lines changed: 15379 additions & 1154 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/chat-stream-mid-turn-reconnect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
"trigger.dev": patch
55
---
66

7-
Chat in the browser now reconnects when the connection drops mid-turn, instead of leaving the reply stuck as if it were still generating. Reports can be fetched as structured data with the `json` format, and the shortest report period is now one minute (`30m`, `1h`, `7d`). The `mint-token` command's help is clearer too: a token minted without `--cap` is read-only, and `--ttl` shows the correct maximum lifetime of 7 days.
7+
Chat in the browser now reconnects when the connection drops mid-turn, instead of leaving the reply stuck as if it were still generating. Reports can be fetched as structured data with the `json` format, and the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`). The `mint-token` command's help is clearer too: a token minted without `--cap` is read-only, and `--ttl` shows the correct maximum lifetime of 7 days.

.claude/agents/code-reviewer.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
name: code-reviewer
3+
description: Adversarially verifies one landed packet against its requirement; read-only.
4+
model: opus
5+
---
6+
7+
You are an adversarial code reviewer for one landed packet. READ-ONLY: never modify code, never commit, never push, never post to GitHub.
8+
9+
- Try to refute that the change answers its stated requirement; look for the failure scenario, not confirmation.
10+
- Check the diff for unrelated drift, dead code, broken semantics of neighbors, and whether tests prove the actual invariant (would the test fail if the fix were subtly wrong?).
11+
- Check the change landed in the correct PR/branch of the stack.
12+
- Distinguish fact from inference; cite exact file:line evidence.
13+
- Return: verdict (approve / needs-changes) with evidence per concern, and the exact minimal correction when needs-changes.

.claude/agents/code-writer.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
name: code-writer
3+
description: Implements exactly one work packet — minimal diff, targeted checks, own-paths-only commits.
4+
model: opus
5+
---
6+
7+
You are a code writer. Implement exactly the one work packet in your prompt.
8+
9+
- Minimal diff; match surrounding style and idiom.
10+
- Prefer no comment at all; comment only a non-obvious constraint, max 2 short lines. All texts (comments, commit messages) short, clear, simple.
11+
- Verify the packet's own diagnosis against the code before applying; if it is wrong, STOP without committing and report why.
12+
- Run only the targeted checks for your packet: the relevant vitest files, `pnpm run typecheck --filter <pkg>` when the change warrants it. Never full suites unless asked.
13+
- `pnpm run format` on touched files before committing.
14+
- Stage and commit ONLY your packet's files. Conventional commit message. NO Claude attribution, no Co-Authored-By.
15+
- Push only if the packet explicitly says to.
16+
- Return: what changed, evidence (test output), commit SHA, and anything contradicting the diagnosis.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
name: software-architect
3+
description: Resolves contested design questions against the specs; decision + rationale, never code.
4+
model: opus
5+
---
6+
7+
You are a software architect. Resolve exactly the contested design question in your prompt against the given specs/contracts. READ-ONLY.
8+
9+
- Ground the decision in the actual code and the project's design contracts (GUIDEBOOK, Linear specs) — not in generic best practice.
10+
- Weigh stack boundaries: which PR owns the change, what merges independently.
11+
- Prefer the smallest decision that unblocks the packet; flag speculative architecture rather than endorsing it.
12+
- Return: the decision, its rationale, rejected alternatives (one line each), and exactly what the dependent packet should do.

apps/webapp/app/components/AskAI.tsx

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
2-
* @deprecated Superseded by the dashboard agent (`components/dashboard-agent`). Nothing mounts
3-
* this any more — every Ask AI entry point now opens Ask Trigger. Kept until the agent has
4-
* shipped, then removed along with `@kapaai/react-sdk` and `KAPA_AI_WEBSITE_ID`.
2+
* Mostly superseded by the dashboard agent (`components/dashboard-agent`), which owns every
3+
* entry point except two: ⌘I and the CLI's `?aiHelp=` link still open Ask AI. `AskAIRoot` is
4+
* mounted by the `_app` layout for those; the `AskAI` button below is mounted nowhere.
55
*/
66

77
import {
@@ -17,13 +17,17 @@ import { useSearchParams } from "@remix-run/react";
1717
import DOMPurify from "dompurify";
1818
import { motion } from "framer-motion";
1919
import { marked } from "marked";
20-
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
21-
import { useTypedRouteLoaderData } from "remix-typedjson";
20+
import { useCallback, useEffect, useRef, useState } from "react";
2221
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
2322
import { SparkleListIcon } from "~/assets/icons/SparkleListIcon";
24-
import { useFeatures } from "~/hooks/useFeatures";
23+
import { useAskAiAvailability } from "~/hooks/useAskAiAvailability";
2524
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
26-
import { type loader } from "~/root";
25+
import {
26+
ASK_AI_DEEP_LINK_PARAM,
27+
ASK_AI_SHORTCUT,
28+
askAiCanOpen,
29+
} from "./dashboard-agent/ask-ai-channels";
30+
import { useAskAiHost } from "./dashboard-agent/askAiOpenRequest";
2731
import { Button } from "./primitives/Buttons";
2832
import { Callout } from "./primitives/Callout";
2933
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./primitives/Dialog";
@@ -40,11 +44,6 @@ import {
4044
} from "./primitives/Tooltip";
4145
import { ClientOnly } from "remix-utils/client-only";
4246

43-
function useKapaWebsiteId() {
44-
const routeMatch = useTypedRouteLoaderData<typeof loader>("root");
45-
return routeMatch?.kapa.websiteId;
46-
}
47-
4847
/** Open/close state for the Ask AI dialog, including the `?aiHelp=` deep-link handling. */
4948
function useAskAIState() {
5049
const [isOpen, setIsOpen] = useState(false);
@@ -67,14 +66,14 @@ function useAskAIState() {
6766

6867
// Handle URL param functionality
6968
useEffect(() => {
70-
const aiHelp = searchParams.get("aiHelp");
69+
const aiHelp = searchParams.get(ASK_AI_DEEP_LINK_PARAM);
7170
if (aiHelp) {
7271
// Delay to avoid hCaptcha bot detection
7372
window.setTimeout(() => openAskAI(aiHelp), 1000);
7473

7574
// Clone instead of mutating in place
7675
const next = new URLSearchParams(searchParams);
77-
next.delete("aiHelp");
76+
next.delete(ASK_AI_DEEP_LINK_PARAM);
7877
setSearchParams(next);
7978
}
8079
}, [searchParams, openAskAI]);
@@ -83,45 +82,30 @@ function useAskAIState() {
8382
}
8483

8584
/**
86-
* Hosts Ask AI (Kapa provider, ⌘I shortcut, dialog) for a menu that renders its own trigger. Wrap
87-
* it around the popover, not inside, so the dialog and shortcut survive the popover closing.
88-
* `children` receives the open function, or undefined when Ask AI is unavailable (self-hosted, no
89-
* Kapa website id, or SSR).
90-
*
91-
* @deprecated See the note at the top of this file.
85+
* Hosts Ask AI (Kapa provider, ⌘I shortcut, dialog). It renders no page content and wraps
86+
* nothing: entry points reach it through `requestAskAi`, so the Kapa provider mounting after
87+
* hydration can never remount the app around it.
9288
*/
93-
export function AskAIRoot({
94-
children,
95-
}: {
96-
children: (openAskAI: (() => void) | undefined) => ReactNode;
97-
}) {
98-
const { isManagedCloud } = useFeatures();
99-
const websiteId = useKapaWebsiteId();
89+
export function AskAIRoot() {
90+
const availability = useAskAiAvailability();
10091

101-
if (!isManagedCloud || !websiteId) {
102-
return <>{children(undefined)}</>;
92+
if (!askAiCanOpen(availability)) {
93+
return null;
10394
}
10495

105-
return (
106-
<ClientOnly fallback={<>{children(undefined)}</>}>
107-
{() => <AskAIRootProvider websiteId={websiteId}>{children}</AskAIRootProvider>}
108-
</ClientOnly>
109-
);
96+
const websiteId = availability.kapaWebsiteId!;
97+
98+
return <ClientOnly>{() => <AskAIRootProvider websiteId={websiteId} />}</ClientOnly>;
11099
}
111100

112-
function AskAIRootProvider({
113-
websiteId,
114-
children,
115-
}: {
116-
websiteId: string;
117-
children: (openAskAI: () => void) => ReactNode;
118-
}) {
101+
function AskAIRootProvider({ websiteId }: { websiteId: string }) {
119102
const { isOpen, setIsOpen, initialQuery, openAskAI, closeAskAI } = useAskAIState();
120103

121104
useShortcutKeys({
122-
shortcut: { modifiers: ["mod"], key: "i", enabledOnInputElements: true },
105+
shortcut: ASK_AI_SHORTCUT,
123106
action: () => openAskAI(),
124107
});
108+
useAskAiHost(openAskAI);
125109

126110
return (
127111
<KapaProvider
@@ -134,7 +118,6 @@ function AskAIRootProvider({
134118
}}
135119
botProtectionMechanism="hcaptcha"
136120
>
137-
{children(() => openAskAI())}
138121
<AskAIDialog
139122
initialQuery={initialQuery}
140123
isOpen={isOpen}
@@ -145,15 +128,16 @@ function AskAIRootProvider({
145128
);
146129
}
147130

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

153-
if (!isManagedCloud || !websiteId) {
135+
if (!askAiCanOpen(availability)) {
154136
return null;
155137
}
156138

139+
const websiteId = availability.kapaWebsiteId!;
140+
157141
return (
158142
<ClientOnly
159143
fallback={

apps/webapp/app/components/BlankStatePanels.tsx

Lines changed: 58 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
v3NewProjectAlertPath,
3636
v3NewSchedulePath,
3737
} from "~/utils/pathBuilder";
38-
import { AskAI } from "./AskAI";
38+
import { AskAgentButton } from "./dashboard-agent/AskAgentButton";
3939
import { CodeBlock } from "./code/CodeBlock";
4040
import { useDevPresence } from "./DevPresence";
4141
import { InlineCode } from "./code/InlineCode";
@@ -65,6 +65,55 @@ import {
6565
import { StepContentContainer } from "./StepContentContainer";
6666
import { V4Badge } from "./V4Badge";
6767

68+
/**
69+
* What the agent is asked when it's opened from a deployment setup panel. The panel is the docs
70+
* answer; the agent is for the part the docs can't answer — this project, this environment.
71+
*/
72+
const ASK_AGENT_DEPLOY_PROMPT =
73+
"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.";
74+
75+
/** The docs links the deployment panels offer to anyone without the agent. */
76+
function DeployDocsLinks() {
77+
return (
78+
<>
79+
<SimpleTooltip
80+
asChild
81+
tabbable
82+
button={
83+
// Span wrapper: LinkButton drops the pointer-event props Radix injects via asChild, so
84+
// the tooltip trigger has to be a plain element (same pattern as FavoritePageButton).
85+
<span className="flex">
86+
<LinkButton
87+
variant="small-menu-item"
88+
LeadingIcon={BookOpenIcon}
89+
leadingIconClassName="text-blue-500"
90+
to={docsPath("deployment/overview")}
91+
aria-label="Deploy docs"
92+
/>
93+
</span>
94+
}
95+
content="Deploy docs"
96+
/>
97+
<SimpleTooltip
98+
asChild
99+
tabbable
100+
button={
101+
<span className="flex">
102+
<LinkButton
103+
variant="small-menu-item"
104+
LeadingIcon={QuestionMarkCircleIcon}
105+
leadingIconClassName="text-blue-500"
106+
to={docsPath("troubleshooting#deployment")}
107+
aria-label="Troubleshooting docs"
108+
/>
109+
</span>
110+
}
111+
content="Troubleshooting docs"
112+
/>
113+
</>
114+
);
115+
}
116+
68117
export function HasNoTasksDev({ initializedAt }: { initializedAt: Date | string | null }) {
69118
const { isConnected } = useDevPresence();
70119
const initialized = !!initializedAt;
@@ -330,29 +379,10 @@ export function DeploymentsNoneDev() {
330379
<Header1>Deploy your tasks</Header1>
331380
</div>
332381
<div className="flex items-center">
333-
<SimpleTooltip
334-
button={
335-
<LinkButton
336-
variant="small-menu-item"
337-
LeadingIcon={BookOpenIcon}
338-
leadingIconClassName="text-blue-500"
339-
to={docsPath("deployment/overview")}
340-
/>
341-
}
342-
content="Deploy docs"
343-
/>
344-
<SimpleTooltip
345-
button={
346-
<LinkButton
347-
variant="small-menu-item"
348-
LeadingIcon={QuestionMarkCircleIcon}
349-
leadingIconClassName="text-blue-500"
350-
to={docsPath("troubleshooting#deployment")}
351-
/>
352-
}
353-
content="Troubleshooting docs"
354-
/>
355-
<AskAI />
382+
{/* One entry point instead of two: the docs links were a guess at which page you
383+
needed, and the agent can look at this project and answer for it. Someone with no
384+
agent still gets the links. */}
385+
<AskAgentButton prompt={ASK_AGENT_DEPLOY_PROMPT} fallback={<DeployDocsLinks />} />
356386
</div>
357387
</div>
358388
<StepNumber stepNumber="→" title="Switch to a deployed environment" />
@@ -718,29 +748,10 @@ function DeploymentOnboardingSteps() {
718748
</Header1>
719749
</div>
720750
<div className="flex items-center">
721-
<SimpleTooltip
722-
button={
723-
<LinkButton
724-
variant="small-menu-item"
725-
LeadingIcon={BookOpenIcon}
726-
leadingIconClassName="text-blue-500"
727-
to={docsPath("deployment/overview")}
728-
/>
729-
}
730-
content="Deploy docs"
731-
/>
732-
<SimpleTooltip
733-
button={
734-
<LinkButton
735-
variant="small-menu-item"
736-
LeadingIcon={QuestionMarkCircleIcon}
737-
leadingIconClassName="text-blue-500"
738-
to={docsPath("troubleshooting#deployment")}
739-
/>
740-
}
741-
content="Troubleshooting docs"
742-
/>
743-
<AskAI />
751+
{/* One entry point instead of two: the docs links were a guess at which page you
752+
needed, and the agent can look at this project and answer for it. Someone with no
753+
agent still gets the links. */}
754+
<AskAgentButton prompt={ASK_AGENT_DEPLOY_PROMPT} fallback={<DeployDocsLinks />} />
744755
</div>
745756
</div>
746757
<ClientTabs defaultValue="github">

0 commit comments

Comments
 (0)