feat(webapp): dashboard agent — UI - #4529
Conversation
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThe 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)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/sdk
commit: |
bd4d4a0 to
887f5b6
Compare
887f5b6 to
17a0f07
Compare
2460d14 to
9447128
Compare
| <AskAIRoot> | ||
| {() => ( | ||
| <AppContainer> | ||
| <Outlet /> | ||
| </AppContainer> | ||
| )} | ||
| </AskAIRoot> |
There was a problem hiding this comment.
🔴 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).
Was this helpful? React with 👍 or 👎 to provide feedback.
| 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."); | ||
| } |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| 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]); |
There was a problem hiding this comment.
🟡 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.
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"; | |||
There was a problem hiding this comment.
🔍 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
ddb9f3c to
88efcf7
Compare
Observability mapAs of 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. |
| <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" /> |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| {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> | ||
| )} |
There was a problem hiding this comment.
🔍 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| 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}</>; |
There was a problem hiding this comment.
🔍 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.
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.
88efcf7 to
83ae7f2
Compare
| {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> | ||
| )} |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| vi.mock("~/services/dashboardAgentDb.server", () => ({ dashboardAgentDb: undefined })); | ||
|
|
||
| const { sweepDashboardAgentInvestigations } = | ||
| await import("~/services/dashboardAgentInvestigationSweep.server"); |
There was a problem hiding this comment.
🟡 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`.
Was this helpful? React with 👍 or 👎 to provide feedback.
| 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()); | ||
| } |
There was a problem hiding this comment.
🔍 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
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.
What's inside
handle.agentPageContexton 47 routes, ~20 lines each.?aiHelp=links keep working.Notes
canAccessDashboardAgent; no behavior change with the flag off.