From 295324586257314c958e2edddbe88e99d5ed72f7 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 31 Mar 2026 10:43:32 -0400 Subject: [PATCH] feat(site): display loaded context files and skills in context indicator tooltip (#23853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders the `last_injected_context` data (AGENTS.md files and skills) from the Chat API in the `ContextUsageIndicator` hover tooltip. On hover, users now see: - **Context files**: basename with full path on title hover, truncation indicator - **Skills**: name and optional description Separated from the existing token usage info by a border divider when both sections are present. Added `max-w-72` to prevent the tooltip from getting too wide. image
Data flow ``` chatQuery.data.last_injected_context → AgentChatPage (AgentChatPageView prop) → AgentChatPageView (ChatPageInput prop) → ChatPageInput (spread into latestContextUsage) → AgentChatInput (contextUsage prop) → ContextUsageIndicator (usage.lastInjectedContext) ```
Files changed | File | Change | |---|---| | `ContextUsageIndicator.tsx` | Add `lastInjectedContext` to interface, render context files and skills sections in tooltip | | `ChatPageContent.tsx` | Thread `lastInjectedContext` prop, spread into context usage object | | `AgentChatPageView.tsx` | Thread `lastInjectedContext` prop to `ChatPageInput` | | `AgentChatPage.tsx` | Pass `chatQuery.data?.last_injected_context` down |
--- site/src/pages/AgentsPage/AgentChatPage.tsx | 1 + .../pages/AgentsPage/AgentChatPageView.tsx | 4 + .../components/AgentChatInput.stories.tsx | 77 ++++++++++++++++- .../AgentsPage/components/ChatPageContent.tsx | 4 +- .../components/ContextUsageIndicator.tsx | 83 ++++++++++++++++++- 5 files changed, 164 insertions(+), 5 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentChatPage.tsx b/site/src/pages/AgentsPage/AgentChatPage.tsx index a8a1d0bac4..0f6aaee97b 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.tsx @@ -1069,6 +1069,7 @@ const AgentChatPage: FC = () => { selectedMCPServerIds={effectiveMCPServerIds} onMCPSelectionChange={handleMCPSelectionChange} onMCPAuthComplete={handleMCPAuthComplete} + lastInjectedContext={chatQuery.data?.last_injected_context} /> ); }; diff --git a/site/src/pages/AgentsPage/AgentChatPageView.tsx b/site/src/pages/AgentsPage/AgentChatPageView.tsx index 3768ef6ae4..04ccabb322 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.tsx @@ -143,6 +143,8 @@ interface AgentChatPageViewProps { // Desktop chat ID (optional). desktopChatId?: string; + + lastInjectedContext?: readonly TypesGen.ChatMessagePart[]; } export const AgentChatPageView: FC = ({ @@ -199,6 +201,7 @@ export const AgentChatPageView: FC = ({ onMCPSelectionChange, onMCPAuthComplete, desktopChatId, + lastInjectedContext, }) => { const [isRightPanelExpanded, setIsRightPanelExpanded] = useState(false); const [dragVisualExpanded, setDragVisualExpanded] = useState( @@ -350,6 +353,7 @@ export const AgentChatPageView: FC = ({ selectedMCPServerIds={selectedMCPServerIds} onMCPSelectionChange={onMCPSelectionChange} onMCPAuthComplete={onMCPAuthComplete} + lastInjectedContext={lastInjectedContext} /> diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx index e739c5ecfa..7f73672b52 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx @@ -3,7 +3,11 @@ import { useEffect, useRef } from "react"; import { expect, fn, userEvent, waitFor, within } from "storybook/test"; import type * as TypesGen from "#/api/typesGenerated"; import type { ChatMessageInputRef } from "#/components/ChatMessageInput/ChatMessageInput"; -import { AgentChatInput, type UploadState } from "./AgentChatInput"; +import { + AgentChatInput, + type AgentContextUsage, + type UploadState, +} from "./AgentChatInput"; const defaultModelConfigID = "model-config-1"; @@ -653,3 +657,74 @@ export const OverflowBadges: Story = { expect(within(popover).getByText("Confluence Cloud")).toBeInTheDocument(); }, }; + +// --------------------------------------------------------------------------- +// Context-usage indicator stories +// --------------------------------------------------------------------------- + +const baseContextUsage: AgentContextUsage = { + usedTokens: 45_000, + contextLimitTokens: 128_000, + inputTokens: 30_000, + outputTokens: 10_000, + cacheReadTokens: 3_000, + cacheCreationTokens: 2_000, + compressionThreshold: 90, +}; + +/** Shows the context-usage ring and token summary tooltip. */ +export const WithContextUsage: Story = { + args: { + contextUsage: baseContextUsage, + }, +}; + +/** Tooltip includes loaded AGENTS.md files and discovered skills. */ +export const WithContextFiles: Story = { + args: { + contextUsage: { + ...baseContextUsage, + lastInjectedContext: [ + { + type: "context-file" as const, + context_file_path: "/home/coder/project/AGENTS.md", + }, + { + type: "context-file" as const, + context_file_path: "/home/coder/project/.claude/docs/WORKFLOWS.md", + context_file_truncated: true, + }, + { + type: "skill" as const, + skill_name: "pull-requests", + skill_description: "Guide for creating and updating pull requests", + }, + { + type: "skill" as const, + skill_name: "deep-review", + skill_description: "Multi-reviewer code review", + }, + ] as TypesGen.ChatMessagePart[], + }, + }, +}; + +/** Context at 95%+ shows the ring in destructive (red) tone. */ +export const ContextNearLimit: Story = { + args: { + contextUsage: { + usedTokens: 124_000, + contextLimitTokens: 128_000, + inputTokens: 100_000, + outputTokens: 20_000, + cacheReadTokens: 4_000, + compressionThreshold: 90, + lastInjectedContext: [ + { + type: "context-file" as const, + context_file_path: "/home/coder/project/AGENTS.md", + }, + ] as TypesGen.ChatMessagePart[], + }, + }, +}; diff --git a/site/src/pages/AgentsPage/components/ChatPageContent.tsx b/site/src/pages/AgentsPage/components/ChatPageContent.tsx index ca8405ef13..467cd8d8c4 100644 --- a/site/src/pages/AgentsPage/components/ChatPageContent.tsx +++ b/site/src/pages/AgentsPage/components/ChatPageContent.tsx @@ -150,6 +150,7 @@ interface ChatPageInputProps { selectedMCPServerIds?: readonly string[]; onMCPSelectionChange?: (ids: string[]) => void; onMCPAuthComplete?: (serverId: string) => void; + lastInjectedContext?: readonly TypesGen.ChatMessagePart[]; } export const ChatPageInput: FC = ({ @@ -182,6 +183,7 @@ export const ChatPageInput: FC = ({ selectedMCPServerIds, onMCPSelectionChange, onMCPAuthComplete, + lastInjectedContext, }) => { const messagesByID = useChatSelector(store, selectMessagesByID); const orderedMessageIDs = useChatSelector(store, selectOrderedMessageIDs); @@ -212,7 +214,7 @@ export const ChatPageInput: FC = ({ const rawUsage = getLatestContextUsage(messages); const latestContextUsage = rawUsage - ? { ...rawUsage, compressionThreshold } + ? { ...rawUsage, compressionThreshold, lastInjectedContext } : rawUsage; const { organizations } = useDashboard(); const organizationId = organizations[0]?.id; diff --git a/site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx b/site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx index 089710c4dc..2bfb100d1f 100644 --- a/site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx +++ b/site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx @@ -1,4 +1,6 @@ +import { FileIcon, ZapIcon } from "lucide-react"; import type { FC } from "react"; +import type { ChatMessagePart } from "#/api/typesGenerated"; import { Popover, PopoverContent, @@ -22,6 +24,8 @@ export interface AgentContextUsage { readonly reasoningTokens?: number; // Percentage (0–100) at which the context will be compacted. readonly compressionThreshold?: number; + // Last injected context parts (AGENTS.md files and skills). + readonly lastInjectedContext?: readonly ChatMessagePart[]; } const hasFiniteTokenValue = (value: number | undefined): value is number => @@ -58,6 +62,12 @@ const getIndicatorToneClassName = (percentUsed: number | null): string => { return "text-content-secondary/60"; }; +/** Extract the trailing filename from an absolute path. */ +const basename = (path: string): string => { + const slash = path.lastIndexOf("/"); + return slash >= 0 ? path.substring(slash + 1) : path; +}; + const RING_SIZE = 18; const RING_STROKE = 2.5; const RING_RADIUS = (RING_SIZE - RING_STROKE) / 2; @@ -91,6 +101,13 @@ export const ContextUsageIndicator: FC<{ usage: AgentContextUsage | null }> = ({ ? `Context usage ${percentLabel}. ${formatTokenCount(usedTokens)} of ${formatTokenCount(contextLimitTokens)} tokens used.` : "Context usage"; + // Extract context files and skills from lastInjectedContext. + const contextFiles = + usage?.lastInjectedContext?.filter((p) => p.type === "context-file") ?? []; + const skills = + usage?.lastInjectedContext?.filter((p) => p.type === "skill") ?? []; + const hasInjectedContext = contextFiles.length > 0 || skills.length > 0; + const triggerButton = (