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, and suggested prompts. Routes now provide agent page context and investigation actions. Documentation links are removed from selected page headers. Tests cover rendering, routing, prompts, navigation, transcript state, quotas, and report parity. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
| // Always undefined until billing supplies plan detection, which means no cap. | ||
| function useIsFreePlan(): boolean | undefined { | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
🔍 Free-plan message cap is permanently inert
useIsFreePlan() always returns undefined, so resolveMessageQuota always yields { kind: "unlimited" }. That means AgentUpgradeBlock, AgentQuotaNotice, the atMessageCap guard in DashboardAgentChat and the ?quota=1 fetch are all dead paths today. Worth confirming this is intentional scaffolding (the comment says billing hasn't supplied plan detection yet) rather than a wiring omission. Note also that if the cap ever activates mid-stream, the composer — and with it the Stop button — is unmounted while a turn is still streaming.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Intentional — the cap is scaffolding until billing supplies plan detection, as the comment on useIsFreePlan says, so every path behind it is inert by design. The composer/Stop ordering when the cap flips mid-turn gets settled when billing wires the flag up.
@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
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (22)
apps/webapp/app/components/Shortcuts.tsx (1)
68-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSource the modifier from
TOGGLE_PANEL_SHORTCUTas well.Line 69 hardcodes
["mod"]while line 70 reads the key from the shared constant. If the modifier ofTOGGLE_PANEL_SHORTCUTchanges, this row displays the wrong combination. The new Chat section at lines 103-107 already reads both parts fromNEW_CHAT_SHORTCUT.apps/webapp/app/components/dashboard-agent/dashboardAgentLauncher.tsxpasses the whole object toShortcutKey, so a single render is enough here.♻️ Proposed fix
<Shortcut name={ASK_AGENT_LABEL}> - <ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" /> - <ShortcutKey shortcut={{ key: TOGGLE_PANEL_SHORTCUT.key }} variant="medium/bright" /> + <ShortcutKey shortcut={TOGGLE_PANEL_SHORTCUT} variant="medium/bright" /> </Shortcut>apps/webapp/app/components/dashboard-agent/agent-identity.ts (2)
1-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTwo entry points use different agent icons.
This module defines
AgentIconas the shared agent mark, andAskAgentButton.tsxuses it. However,apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsxline 133 rendersAgentMonoLogofor the sameASK_AGENT_LABELaction. The agent therefore appears with two different icons across entry points. Pick one mark and export it from this module, or document why the popover differs.
3-3: 📐 Maintainability & Code Quality | 🔵 TrivialTracked TODO: final agent icon.
TODO(TRI-12763)records that the placeholder icon must be replaced. The placeholder ships to users in the meantime. Confirm that TRI-12763 is scheduled before this feature is enabled for all plans.Do you want me to open a follow-up issue that links this line to TRI-12763?
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx (1)
91-94: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove the
handleexport below the last import.The
pageMetaimport at Line 94 sits after thehandledeclaration. The code still runs, because ES module imports are hoisted. However, this ordering is hard to read andimport/firststyle rules flag it.♻️ Proposed reordering
import { sectionAgentPageContext } from "~/components/dashboard-agent/suggested-prompts"; import type { Handle } from "~/utils/handle"; +import { pageMeta } from "~/utils/pageTitle"; export const handle: Handle = { agentPageContext: () => sectionAgentPageContext("envvars"), }; -import { pageMeta } from "~/utils/pageTitle"; export const meta = pageMeta("Environment variables");apps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.ts (1)
134-157: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueGuard against a non-finite burst limit.
z.number()acceptsNaNandInfinityin Zod 3. IfburstFactororconcurrencyLimitarrives asNaN,limitbecomesNaNand the comparison silently disables the saturation signal. Usez.number().finite()for these fields to make the contract explicit.♻️ Proposed schema tightening
const queuesLoaderDataSchema = z.object({ environment: z.object({ - running: z.number(), - queued: z.number(), - concurrencyLimit: z.number(), - burstFactor: z.number().nullish(), + running: z.number().finite(), + queued: z.number().finite(), + concurrencyLimit: z.number().finite(), + burstFactor: z.number().finite().nullish(), }), });apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsx (1)
21-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDeclare
handleafter the import block in four routes. Each of these routes inserts theexport const handledeclaration betweenimportstatements, leavingimport { pageMeta } from "~/utils/pageTitle"below it. The code runs because ESM hoists imports, but the split import block is inconsistent with the other routes in this PR (query,regions,runs.$runParam,schedules.$scheduleParam,sessions.$sessionParam,queues), which declarehandleafter all imports.
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsx#L21-L26: move thehandledeclaration below thepageMetaimport.apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx#L55-L60: move thehandledeclaration below thepageMetaimport.apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx#L77-L82: move thehandledeclaration below thepageMetaimport.apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx#L50-L55: move thehandledeclaration below thepageMetaimport.apps/webapp/app/components/dashboard-agent/view-actions.test.ts (1)
50-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider replacing source-text assertions with behavior tests.
These assertions read
ActionsBlock.tsxas a string. A formatting change or a rename breaks them without a behavior change, and they do not prove the component renders or dispatches correctly. If a React renderer is already available in this suite, assert on rendered output and on theonIntentcallback instead.apps/webapp/app/components/dashboard-agent/AgentChart.tsx (1)
50-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting the shared action-button row.
ChartActionsduplicatesActionsBlockinapps/webapp/app/components/dashboard-agent/ActionsBlock.tsx(lines 9-31). Both filter withrenderableActions, key by index, pickprimary/smallfor the first item, and cast withas AgentIntent. One shared component keeps the styling rules and the cast in one place.♻️ Suggested shared component
// chat-layout.tsx (or a new agent-actions.tsx) export function AgentActionButtons({ actions, onIntent, }: { actions: readonly { label: string; intent: unknown }[]; onIntent?: (intent: AgentIntent) => void; }) { const renderable = renderableActions(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> ); }apps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx (1)
52-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider sharing the default page context.
The literal
{ page: { kind: "other", path: "" }, signals: [] }also appears inapps/webapp/app/components/dashboard-agent/DashboardAgentDraft.tsx. Both call sites must resolve the same prompts, and the draft comment states that intent. Export one constant from thesuggested-promptsmodule so the two stay in sync.♻️ Suggested change
+import { + DEFAULT_AGENT_PAGE_CONTEXT, + readDismissedPromptIds, + resolveSuggestedPromptsBySlot, + type ResolvedPromptSlot, +} from "./suggested-prompts"; + const prompts = useMemo( () => - resolveSuggestedPromptsBySlot( - pageContext ?? { page: { kind: "other", path: "" }, signals: [] }, - { promoted, dismissedIds: effectiveDismissedIds } - ), + resolveSuggestedPromptsBySlot(pageContext ?? DEFAULT_AGENT_PAGE_CONTEXT, { + promoted, + dismissedIds: effectiveDismissedIds, + }), [pageContext, promoted, effectiveDismissedIds] );apps/webapp/app/components/dashboard-agent/view-catalog.tsx (1)
28-31: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider precomputing the original indices.
blocks.indexOf(block)runs a linear scan for every rendered block, so the loop is O(n²). It also returns the first index if one block object instance appears twice inblocks, which yields duplicate keys. A single index map removes both concerns.♻️ Suggested change
- {latestRevisionBlocks(blocks).map((block) => { - // Index into the original array, so collapsing a revision above an - // envelope-less block can't shift its key. - const key = blockKey(block, blocks.indexOf(block)); + {(() => { + // Index into the original array, so collapsing a revision above an + // envelope-less block can't shift its key. + const originalIndex = new Map(blocks.map((block, index) => [block, index])); + return latestRevisionBlocks(blocks).map((block) => { + const key = blockKey(block, originalIndex.get(block) ?? 0);apps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsx (1)
5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider moving
toSafeUrlto a shared utility module.
toSafeUrlis a URL sanitizer, but it is imported from the view module~/components/runs/v3/agent/AgentMessageView. This file now uses it for both evidence references and documentation targets. A dedicated util module keeps the security helper independent of a rendering component.This is a placement concern only. The current behavior is correct.
Also applies to: 114-136
apps/webapp/app/components/dashboard-agent/DashboardAgentComposer.tsx (1)
121-132: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMount the live region before the counter appears.
The element that carries
aria-live="polite"is added to the DOM only when the count reachesMESSAGE_CHARS_WARN_AT. Several screen readers announce updates only for live regions that already exist in the DOM. Keep the region mounted and change its content instead.♿ Proposed change
- {/* Only near the limit: a normal message never sees a counter. */} - {value.length >= MESSAGE_CHARS_WARN_AT ? ( - <p - className={cn( - "self-end text-xxs tabular-nums", - value.length >= MAX_MESSAGE_CHARS ? "text-error" : "text-text-dimmed" - )} - aria-live="polite" - > - {value.length} / {MAX_MESSAGE_CHARS} - </p> - ) : null} + {/* Only near the limit: a normal message never sees a counter. */} + <p + className={cn( + "self-end text-xxs tabular-nums", + value.length >= MAX_MESSAGE_CHARS ? "text-error" : "text-text-dimmed", + value.length < MESSAGE_CHARS_WARN_AT && "hidden" + )} + aria-live="polite" + > + {value.length >= MESSAGE_CHARS_WARN_AT ? `${value.length} / ${MAX_MESSAGE_CHARS}` : ""} + </p>apps/webapp/app/components/dashboard-agent/DashboardAgentDraft.tsx (1)
29-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueShare the empty page-context default and forward
dismissedIds.Two points:
- The fallback literal
{ page: { kind: "other", path: "" }, signals: [] }also exists inapps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsx. Export one named constant from the suggested-prompts module and use it in both places.dismissedIdsis not passed toDashboardAgentHero, soDashboardAgentSuggestedPromptsreadslocalStoragea second time. Forward the value the draft already read to keep one source.As per coding guidelines: "Use named constants for sentinel or placeholder values instead of scattering raw string literals across comparisons."
♻️ Proposed change
const placeholderSuggestion = useMemo( () => - resolveSuggestedPromptsBySlot( - pageContext ?? { page: { kind: "other", path: "" }, signals: [] }, - { - promoted: promotedPrompt, - dismissedIds, - } - )[0]?.prompt.prompt, + resolveSuggestedPromptsBySlot(pageContext ?? EMPTY_PAGE_CONTEXT, { + promoted: promotedPrompt, + dismissedIds, + })[0]?.prompt.prompt, [pageContext, promotedPrompt, dismissedIds] ); @@ <DashboardAgentHero onSelect={submit} pageContext={pageContext} promoted={promotedPrompt} + dismissedIds={dismissedIds}Source: Coding guidelines
apps/webapp/app/components/dashboard-agent/report-block-adapter.test.ts (2)
94-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe assertion does not prove that
seriesis dropped.The test name states that the curated output drops links and series. The
vmfixture has noserieson its metric, soexpect(block.vm.metrics[0]!.series).toBeUndefined()passes even if the adapter kept the field. Addseriesto the metric in the input to make the assertion meaningful.💚 Proposed change
it("accepts the curated tool output, which drops links and series", () => { const { links, ...curated } = vm; - const block = reportBlockFromToolPart(part({ output: { ...curated, seriesOmitted: true } }))!; + const block = reportBlockFromToolPart( + part({ + output: { + ...curated, + metrics: [{ ...curated.metrics[0]!, series: [1, 2, 3] }], + seriesOmitted: true, + }, + }) + )!; expect(block.vm.links).toEqual([]); expect(block.vm.metrics[0]!.series).toBeUndefined();
118-127: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
sameKeyAgainuses a different tool call id.The variable name states that the key repeats, but the call passes
toolCallId: "call_3". The assertion therefore repeats the distinct-id case instead of covering a repeated id. Rename the variable, or pass"call_1"and assert the expected collapse behavior.apps/webapp/app/components/dashboard-agent/useAgentMessageQuota.ts (1)
5-8: 📐 Maintainability & Code Quality | 🔵 Trivial
useIsFreePlanalways returnsundefined, so the whole quota path is inert.
resolveMessageQuotareturns{ kind: "unlimited" }wheneverisFreePlan !== true. The effect at Line 25 also returns early, so the quota endpoint is never called. As a resultAgentUpgradeBlockandAgentQuotaNoticeinDashboardAgentChat.tsxnever render, andatMessageCapis alwaysfalse.The comment records this as intentional until billing supplies plan detection. Confirm that shipping the free-plan cap as inactive is the intended state for this PR. I can open a follow-up issue to track wiring plan detection if that helps.
apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx (2)
11-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse the registered shortcut constant instead of redefining it.
NEW_CHAT_SHORTCUTduplicates the ⌘J definition thatDashboardAgentregisters asTOGGLE_PANEL_SHORTCUT. If one definition changes, the displayed key and the registered key diverge. Export the single source from the module that registers it, and import it here for display.
57-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAccessible name hides the visible label.
The trigger renders the chat title as visible text, but
aria-label="Chat history"replaces that name. Speech-input users cannot activate the control by the visible text. Include the title in the accessible name.♿ Proposed change
- aria-label="Chat history" + aria-label={`Chat history: ${title}`} title={title}apps/webapp/app/components/dashboard-agent/ask-ai-channels.ts (1)
51-63: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueConsider rejecting absolute values for
environmentPath.
new URL(environmentPath, origin)ignoresoriginwhenenvironmentPathis absolute. The result then feedsredirect(...)inapps/webapp/app/routes/projects.$projectRef.ai-help.ts. The current caller passes a builder-generated internal path, so there is no exploit today. A guard keeps the function safe if a future caller forwards request data.🛡️ Proposed guard
const url = new URL(environmentPath, origin); + if (url.origin !== new URL(origin).origin) { + throw new Error("environmentPath must be relative to the given origin"); + } url.searchParams.set(ASK_AI_DEEP_LINK_PARAM, query);apps/webapp/app/components/dashboard-agent/InvestigationCard.test.ts (1)
38-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider relaxing the exact-source assertion.
Line 39 matches one exact source line, including spacing. Prettier reflow, a rename of
onIntentoractions, or an equivalent early return breaks this test without any behavior change. The other assertions in this file are pattern-based and tolerate that. Consider matching a looser pattern, or asserting the behavior by rendering the component with and withoutonIntent.♻️ Looser pattern
- expect(source).toMatch(/if \(!onIntent \|\| actions\.length === 0\) return null;/); + expect(source).toMatch(/!onIntent[\s\S]{0,40}return null/);apps/webapp/app/components/dashboard-agent/report-sparkline.tsx (2)
468-475: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe
Math.max(..., 1)guard sits on the wrong operand.Line 472 applies
Math.max(..., 1)to the slice end index. The guard reads as protection against an empty slice, but it cannot help: the function returns early whenpoints.length <= maxBars, soperBar > 1andMath.floor((i + 1) * perBar)is always at least 1. The expression is dead and misleading. The real empty-slice guard is already on line 473.♻️ Proposed simplification
- const slice = points.slice(Math.floor(i * perBar), Math.max(Math.floor((i + 1) * perBar), 1)); + const slice = points.slice(Math.floor(i * perBar), Math.floor((i + 1) * perBar)); return slice.reduce((sum, v) => sum + v, 0) / Math.max(slice.length, 1);
528-544: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
Date.now()during render breaks the stated purity of this file.The file header states no Remix hooks and no host state, and
ReportView.test.tsenforces that for the sibling card. Line 532 reads the wall clock during render. Two consequences follow. Server and client renders produce differentdatevalues for the same data. Every re-render shifts the synthesised bar timestamps, so a tooltip can report a different time for the same bar.The bars themselves do not depend on the clock, only the tooltip labels do. Consider passing the series end time in through the view model, so the timestamps come from the data instead of the render.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c2923ee0-77ad-49e0-97df-d95110a2b67d
📒 Files selected for processing (155)
.server-changes/dashboard-agent.mdapps/webapp/app/components/AskAI.tsxapps/webapp/app/components/BlankStatePanels.tsxapps/webapp/app/components/Shortcuts.tsxapps/webapp/app/components/dashboard-agent/ActionsBlock.tsxapps/webapp/app/components/dashboard-agent/AgentChart.tsxapps/webapp/app/components/dashboard-agent/AgentUpgradeGate.tsxapps/webapp/app/components/dashboard-agent/AskAgentButton.tsxapps/webapp/app/components/dashboard-agent/DashboardAgent.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentComposer.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentContextBanner.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentDraft.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentHero.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentHistory.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentMessages.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentSuggestedPrompts.tsxapps/webapp/app/components/dashboard-agent/InvestigateButton.tsxapps/webapp/app/components/dashboard-agent/InvestigationCard.test.tsapps/webapp/app/components/dashboard-agent/InvestigationCard.tsxapps/webapp/app/components/dashboard-agent/ReportView.test.tsapps/webapp/app/components/dashboard-agent/ReportView.tsxapps/webapp/app/components/dashboard-agent/RunDiagnosisCard.tsxapps/webapp/app/components/dashboard-agent/agent-badges.tsxapps/webapp/app/components/dashboard-agent/agent-card.tsxapps/webapp/app/components/dashboard-agent/agent-identity.tsapps/webapp/app/components/dashboard-agent/agent-shortcuts.test.tsapps/webapp/app/components/dashboard-agent/ask-ai-channels.test.tsapps/webapp/app/components/dashboard-agent/ask-ai-channels.tsapps/webapp/app/components/dashboard-agent/chat-layout.test.tsapps/webapp/app/components/dashboard-agent/chat-layout.tsxapps/webapp/app/components/dashboard-agent/dashboardAgentLauncher.tsxapps/webapp/app/components/dashboard-agent/dashboardAgentOpenRequest.tsapps/webapp/app/components/dashboard-agent/demo/fixtures/page-context.tsapps/webapp/app/components/dashboard-agent/demo/fixtures/reports.tsapps/webapp/app/components/dashboard-agent/demo/ids.tsapps/webapp/app/components/dashboard-agent/diagnosis-actions.test.tsapps/webapp/app/components/dashboard-agent/diagnosis-actions.tsapps/webapp/app/components/dashboard-agent/investigate-prompts.test.tsapps/webapp/app/components/dashboard-agent/investigate-prompts.tsapps/webapp/app/components/dashboard-agent/investigation-winners.test.tsapps/webapp/app/components/dashboard-agent/investigation-winners.tsapps/webapp/app/components/dashboard-agent/list-row.tsxapps/webapp/app/components/dashboard-agent/message-order.test.tsapps/webapp/app/components/dashboard-agent/message-order.tsapps/webapp/app/components/dashboard-agent/message-quota.test.tsapps/webapp/app/components/dashboard-agent/message-quota.tsapps/webapp/app/components/dashboard-agent/model-markdown.test.tsapps/webapp/app/components/dashboard-agent/model-markdown.tsapps/webapp/app/components/dashboard-agent/navigate-target.test.tsapps/webapp/app/components/dashboard-agent/navigate-target.tsapps/webapp/app/components/dashboard-agent/opened-chat.test.tsapps/webapp/app/components/dashboard-agent/opened-chat.tsapps/webapp/app/components/dashboard-agent/page-context-types.tsapps/webapp/app/components/dashboard-agent/page-label.test.tsapps/webapp/app/components/dashboard-agent/page-label.tsapps/webapp/app/components/dashboard-agent/panel-layout.tsxapps/webapp/app/components/dashboard-agent/pending-intents.test.tsapps/webapp/app/components/dashboard-agent/pending-intents.tsapps/webapp/app/components/dashboard-agent/progress-line.test.tsapps/webapp/app/components/dashboard-agent/progress-line.tsapps/webapp/app/components/dashboard-agent/report-block-adapter.test.tsapps/webapp/app/components/dashboard-agent/report-block-adapter.tsapps/webapp/app/components/dashboard-agent/report-sparkline.tsxapps/webapp/app/components/dashboard-agent/run-id.test.tsapps/webapp/app/components/dashboard-agent/run-id.tsapps/webapp/app/components/dashboard-agent/settled-transcript.test.tsapps/webapp/app/components/dashboard-agent/settled-transcript.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/dismissal.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/docs-prompts.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/index.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.test.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/page-mappers.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/page-prompts.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/promoted.test.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/promoted.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/promotedPrompt.server.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/prompt-chips.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/registry.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/resolver.test.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/resolver.tsapps/webapp/app/components/dashboard-agent/suggested-prompts/signal-prompts.tsapps/webapp/app/components/dashboard-agent/tool-labels.test.tsapps/webapp/app/components/dashboard-agent/tool-labels.tsapps/webapp/app/components/dashboard-agent/turn-error.test.tsapps/webapp/app/components/dashboard-agent/turn-error.tsapps/webapp/app/components/dashboard-agent/useAgentMessageQuota.tsapps/webapp/app/components/dashboard-agent/useTranscriptAutoScroll.tsapps/webapp/app/components/dashboard-agent/useTriggerUriResolver.tsapps/webapp/app/components/dashboard-agent/view-actions.test.tsapps/webapp/app/components/dashboard-agent/view-actions.tsapps/webapp/app/components/dashboard-agent/view-blocks.test.tsapps/webapp/app/components/dashboard-agent/view-blocks.tsapps/webapp/app/components/dashboard-agent/view-catalog.tsxapps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsxapps/webapp/app/env.server.tsapps/webapp/app/hooks/useAgentPageContext.tsapps/webapp/app/hooks/useAskAiAvailability.tsapps/webapp/app/hooks/useShortcutKeys.tsxapps/webapp/app/root.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev-branches/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.compare/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam/route.tsxapps/webapp/app/routes/_app/route.tsxapps/webapp/app/routes/projects.$projectRef.ai-help.tsapps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsxapps/webapp/app/utils/handle.tsapps/webapp/test/dashboardAgentInvestigationSweepCard.test.tsapps/webapp/test/reportRenderParity.test.tsapps/webapp/test/taskCodeSnippets.test.ts
💤 Files with no reviewable changes (1)
- apps/webapp/app/root.tsx
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.
887f5b6 to
17a0f07
Compare
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.