diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 02d5a5d83..ab8ce074e 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -14,7 +14,7 @@ import { store } from "./store";
const preview: Preview = {
decorators: [
- withRouter,
+ withRouter, // TODO: Fix the story crash on frequent rerendering
(Story: StoryFn, context): JSX.Element => {
const [isInitialized, setIsInitialized] = useState(false);
const theme = context.globals.theme as Theme;
diff --git a/src/components/Agentic/IncidentDetails/IncidentSummary/index.tsx b/src/components/Agentic/IncidentDetails/IncidentSummary/index.tsx
index 52b467592..9de277e7d 100644
--- a/src/components/Agentic/IncidentDetails/IncidentSummary/index.tsx
+++ b/src/components/Agentic/IncidentDetails/IncidentSummary/index.tsx
@@ -2,15 +2,21 @@ import { IncidentSummaryRecord } from "./IncidentSummaryRecord";
import * as s from "./styles";
import type { IncidentSummaryProps } from "./types";
-export const IncidentSummary = ({ records }: IncidentSummaryProps) => (
-
- {records.map((record) => (
-
- ))}
-
-);
+export const IncidentSummary = ({ records }: IncidentSummaryProps) => {
+ if (records.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {records.map((record) => (
+
+ ))}
+
+ );
+};
diff --git a/src/components/Agentic/common/AgentChat/AgentChat.stories.tsx b/src/components/Agentic/common/AgentChat/AgentChat.stories.tsx
new file mode 100644
index 000000000..9f2d889e6
--- /dev/null
+++ b/src/components/Agentic/common/AgentChat/AgentChat.stories.tsx
@@ -0,0 +1,93 @@
+import type { Meta, StoryObj } from "@storybook/react-webpack5";
+import { useEffect, useState } from "react";
+import { fn } from "storybook/test";
+import { AgentChat } from ".";
+import type { IncidentAgentEvent } from "../../../../redux/services/types";
+
+// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
+const meta: Meta = {
+ title: "Agentic/common/AgentChat",
+ component: AgentChat,
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
+ layout: "fullscreen"
+ }
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
+const mockedAgentEvents: IncidentAgentEvent[] = [
+ {
+ id: "1",
+ type: "human",
+ message: "Can you help me understand why my API response time is slow?",
+ agent_name: "user"
+ },
+ {
+ id: "2",
+ type: "token",
+ message: "Let me analyze your application's performance data...",
+ agent_name: "agent"
+ },
+ {
+ id: "3",
+ type: "token",
+ message:
+ "I've found several performance issues in your application:\n\n1. **Database Query Optimization**: Your user lookup queries are taking an average of 2.3 seconds\n2. **Memory Usage**: High memory allocation in the user service\n3. **Cache Misses**: 78% cache miss rate on user data\n\nWould you like me to suggest specific optimizations for any of these areas?",
+ agent_name: "agent"
+ },
+ {
+ id: "4",
+ type: "tool",
+ agent_name: "agent",
+ message:
+ '\n```json\n{\n "success": false,\n "blockers": "Limited tool access prevents thorough investigation of system-wide issues, infrastructure problems, service mesh configurations, and external dependencies. Need access to Kubernetes API, service mesh telemetry, network monitoring tools, and container logs.",\n "result": {\n "is_relevant": true,\n "objective_success": false,\n "blockers": "Limited tool access prevents thorough investigation of system-wide issues, infrastructure problems, service mesh configurations, and external dependencies. Need access to Kubernetes API, service mesh telemetry, network monitoring tools, and container logs.",\n "beyond_the_result": {\n "summary": "Unable to fully investigate alternative causes due to tool limitations, but analysis suggests potential issues in service mesh, network policies, or external dependencies",\n "description": "The investigation revealed a severe performance degradation (2650%) in the PipelineConnector Execute operation that has been ongoing for over 24 hours. While direct investigation was limited by tool access, the pattern and severity suggest potential issues with service mesh routing, network policies, cross-namespace communication, or external service dependencies rather than simple resource constraints.",\n "confidence_level": "30",\n "confidence_level_reason": "Limited tool access prevents thorough investigation of infrastructure and network-related causes. The assessment is based primarily on timing patterns and service impact analysis rather than direct evidence."\n },\n "next_steps_suggestions": "1. Request access to Kubernetes cluster information and API\\n2. Obtain access to service mesh telemetry and dashboard\\n3. Deploy network monitoring tools\\n4. Enable access to container logs\\n5. Once access is granted, conduct thorough analysis of namespace configurations, service mesh settings, network policies, and external service dependencies",\n "actions_taken": [\n {\n "action": "Gathered relevant objects",\n "action_execution_success": true,\n "action_command": "list_relevant_incident_objects",\n "resolution_success_status": "PARTIAL",\n "resolution_explanation": "Successfully identified the critical trace ID but couldn\'t gather infrastructure-related objects",\n "resolution_success_evidence": "Retrieved trace ID FB0C56FA98816BBBFBB934CCEDEA72E4 showing the performance degradation",\n "state_changes_confirmed_due_to_actions": "Confirmed existence of trace showing 2650% performance degradation in PipelineConnector Execute operation"\n },\n {\n "action": "Tracked relevant trace",\n "action_execution_success": true,\n "action_command": "track_incident_relevant_object",\n "resolution_success_status": "PARTIAL",\n "resolution_explanation": "Successfully tracked the critical trace ID for future reference",\n "resolution_success_evidence": "Trace ID FB0C56FA98816BBBFBB934CCEDEA72E4 was successfully tracked",\n "state_changes_confirmed_due_to_actions": "Added trace to tracked objects for future investigation"\n }\n ]\n }\n}\n```\n',
+ tool_name: "kubernetes_resolution_expert_tool",
+ mcp_name: "",
+ status: "success"
+ },
+ {
+ id: "5",
+ type: "token",
+ message:
+ "Here are my recommendations for optimizing your database queries:\n\n```sql\n-- Add an index on the email column\nCREATE INDEX idx_users_email ON users(email);\n\n-- Use prepared statements\nSELECT id, name, email FROM users WHERE email = ?\n```\n\nThis should reduce your query time from 2.3s to under 100ms.",
+ agent_name: "agent"
+ }
+];
+
+const EVENTS_CURSOR = 2;
+const EVENTS_TIMEOUT = 2000;
+
+export const Default: Story = {
+ args: {
+ data: mockedAgentEvents.slice(0, EVENTS_CURSOR),
+ isDataLoading: false,
+ onMessageSend: fn(),
+ typeInitialMessages: false
+ },
+ render: (args) => {
+ const [events, setEvents] = useState(args.data ?? []);
+
+ useEffect(() => {
+ // Simulate messages arriving progressively
+ const timeouts: number[] = [];
+
+ mockedAgentEvents.slice(EVENTS_CURSOR).forEach((event, index) => {
+ const timeout = window.setTimeout(() => {
+ setEvents((prev) => [...prev, event]);
+ }, index * EVENTS_TIMEOUT);
+
+ timeouts.push(timeout);
+ });
+
+ return () => {
+ timeouts.forEach((timeout) => clearTimeout(timeout));
+ };
+ }, []);
+
+ return ;
+ }
+};
diff --git a/src/components/Agentic/common/AgentEventsList/index.tsx b/src/components/Agentic/common/AgentEventsList/index.tsx
index 8de6f6da4..3b80c51af 100644
--- a/src/components/Agentic/common/AgentEventsList/index.tsx
+++ b/src/components/Agentic/common/AgentEventsList/index.tsx
@@ -1,71 +1,101 @@
import { useEffect, useMemo, useState } from "react";
-import { isNumber } from "../../../../typeGuards/isNumber";
+import type { IncidentAgentEvent } from "../../../../redux/services/types";
import { AgentEvent } from "./AgentEvent";
-import type { AgentEventsListProps } from "./types";
+import type { AgentEventsListProps, RenderState } from "./types";
+
+const isTypingEvent = (event: IncidentAgentEvent) =>
+ ["ai", "token"].includes(event.type);
export const AgentEventsList = ({
events,
onNavigateToIncident,
typeInitialEvents
}: AgentEventsListProps) => {
- const [initialEventsCount] = useState(
- typeInitialEvents ? 0 : events.length
- );
- const [eventsVisibleCount, setEventsVisibleCount] = useState(
+ const [initialVisibleCount] = useState(() =>
typeInitialEvents ? 0 : events.length
);
- const agentEventsIndexes = useMemo(
- () =>
- events.reduce((acc, event, index) => {
- if (["ai", "token"].includes(event.type)) {
- acc.push(index);
- }
- return acc;
- }, [] as number[]),
- [events]
- );
+ const [renderState, setRenderState] = useState({
+ currentEventIndex: initialVisibleCount - 1,
+ isTyping: false
+ });
const handleEventTypingComplete = (id: string) => {
- const i = events.findIndex((x) => x.id === id);
+ const completedEventIndex = events.findIndex((event) => event.id === id);
- const nextAgentEventIndex = agentEventsIndexes.find((el) => el > i);
+ const nextTypingEventIndex = events.findIndex(
+ (event, index) => index > completedEventIndex && isTypingEvent(event)
+ );
- if (isNumber(nextAgentEventIndex)) {
- setEventsVisibleCount(nextAgentEventIndex + 1);
+ if (nextTypingEventIndex !== -1) {
+ setRenderState({
+ currentEventIndex: nextTypingEventIndex,
+ isTyping: true
+ });
} else {
- setEventsVisibleCount(events.length);
+ setRenderState({
+ currentEventIndex: events.length - 1,
+ isTyping: false
+ });
}
};
- const visibleEvents = useMemo(
- () => events.slice(0, eventsVisibleCount),
- [events, eventsVisibleCount]
- );
-
const shouldShowTypingForEvent = (id: string) => {
- const index = visibleEvents.findIndex((x) => x.id === id);
+ const eventIndex = events.findIndex((event) => event.id === id);
+ return (
+ eventIndex >= initialVisibleCount &&
+ eventIndex === renderState.currentEventIndex &&
+ renderState.isTyping
+ );
+ };
- if (index < 0) {
- return false;
+ // Handle new events
+ useEffect(() => {
+ if (renderState.currentEventIndex >= events.length - 1) {
+ return;
}
- return index >= initialEventsCount;
- };
+ if (renderState.isTyping) {
+ return;
+ }
- useEffect(() => {
- if (events.length > eventsVisibleCount) {
- const nextAgentEventIndex = agentEventsIndexes.find(
- (index) => index >= eventsVisibleCount
- );
+ const nextEventIndex = renderState.currentEventIndex + 1;
+ const nextEvent = events[nextEventIndex];
+
+ const isInitialEvent = nextEventIndex < initialVisibleCount;
- if (isNumber(nextAgentEventIndex)) {
- setEventsVisibleCount(nextAgentEventIndex + 1);
- } else {
- setEventsVisibleCount(events.length);
- }
+ // Start typing if
+ // either it's an initial event with typeInitialEvents=true
+ // or
+ // it's a new event
+ if (
+ isTypingEvent(nextEvent) &&
+ ((isInitialEvent && typeInitialEvents) || !isInitialEvent)
+ ) {
+ setRenderState({
+ currentEventIndex: nextEventIndex,
+ isTyping: true
+ });
+ return;
}
- }, [events.length, eventsVisibleCount, agentEventsIndexes]);
+
+ // Otherwise, show the next event immediately
+ setRenderState((prev) => ({
+ ...prev,
+ currentEventIndex: nextEventIndex
+ }));
+ }, [
+ events,
+ renderState.currentEventIndex,
+ renderState.isTyping,
+ initialVisibleCount,
+ typeInitialEvents
+ ]);
+
+ const visibleEvents = useMemo(
+ () => events.slice(0, renderState.currentEventIndex + 1),
+ [events, renderState.currentEventIndex]
+ );
return visibleEvents.map((event) => (
void;
typeInitialEvents?: boolean;
}
+
+export interface RenderState {
+ currentEventIndex: number;
+ isTyping: boolean;
+}
diff --git a/src/components/Agentic/common/AgentFlowChart/index.tsx b/src/components/Agentic/common/AgentFlowChart/index.tsx
index a4fa5662f..c462c529b 100644
--- a/src/components/Agentic/common/AgentFlowChart/index.tsx
+++ b/src/components/Agentic/common/AgentFlowChart/index.tsx
@@ -10,18 +10,28 @@ import type {
import { AgentFlowChartNodeToolbar } from "./AgentFlowChartNodeToolbar";
import type { AgentFlowChartProps, ExtendedAgent } from "./types";
+const isAgentWaitingOrSkipped = (
+ agents: ExtendedAgent[],
+ agentName: string
+) => {
+ const agent = agents.find((a) => a.name === agentName);
+ return agent ? ["waiting", "skipped"].includes(agent.status) : undefined;
+};
+
const getFlowChartNodeData = ({
agent,
isSelected,
isInteractive,
+ isDisabled,
isEditMode,
onAddMCPServer,
onEditMCPServer,
onDeleteMCPServer
}: {
agent?: ExtendedAgent;
- isInteractive?: boolean;
isSelected?: boolean;
+ isInteractive?: boolean;
+ isDisabled?: boolean;
isEditMode?: boolean;
onAddMCPServer?: (agentName: string) => void;
onEditMCPServer?: (agentName: string, server: string) => void;
@@ -69,7 +79,7 @@ const getFlowChartNodeData = ({
isPending: agent.status === "pending",
hasError: agent.status === "error",
isInteractive,
- isDisabled: agent.status === "skipped",
+ isDisabled,
sideContainers: [
{
isVisible: Boolean(agent.mcp_servers.length > 0 || isEditMode),
@@ -86,8 +96,7 @@ const getFlowChartNodeData = ({
/>
)
}
- ],
- isKebabMenuVisible: isEditMode
+ ]
}
: {};
};
@@ -186,8 +195,11 @@ export const AgentFlowChartComponent = ({
agent: extendedAgents?.find((a) => a.name === "watchman"),
isSelected: "watchman" === selectedAgentId,
isInteractive:
- extendedAgents?.find((a) => a.name === "watchman")?.status !==
- "skipped",
+ Boolean(isEditMode) ||
+ !isAgentWaitingOrSkipped(extendedAgents, "watchman"),
+ isDisabled:
+ !isEditMode &&
+ isAgentWaitingOrSkipped(extendedAgents, "watchman"),
isEditMode,
onAddMCPServer,
onEditMCPServer,
@@ -204,8 +216,10 @@ export const AgentFlowChartComponent = ({
agent: extendedAgents?.find((a) => a.name === "triager"),
isSelected: "triager" === selectedAgentId,
isInteractive:
- extendedAgents?.find((a) => a.name === "triager")?.status !==
- "skipped",
+ Boolean(isEditMode) ||
+ !isAgentWaitingOrSkipped(extendedAgents, "triager"),
+ isDisabled:
+ !isEditMode && isAgentWaitingOrSkipped(extendedAgents, "triager"),
isEditMode,
onAddMCPServer,
onEditMCPServer,
@@ -222,8 +236,11 @@ export const AgentFlowChartComponent = ({
agent: extendedAgents?.find((a) => a.name === "infra_resolver"),
isSelected: "infra_resolver" === selectedAgentId,
isInteractive:
- extendedAgents?.find((a) => a.name === "infra_resolver")
- ?.status !== "skipped",
+ Boolean(isEditMode) ||
+ !isAgentWaitingOrSkipped(extendedAgents, "infra_resolver"),
+ isDisabled:
+ !isEditMode &&
+ isAgentWaitingOrSkipped(extendedAgents, "infra_resolver"),
isEditMode,
onAddMCPServer,
onEditMCPServer,
@@ -240,8 +257,11 @@ export const AgentFlowChartComponent = ({
agent: extendedAgents?.find((a) => a.name === "code_resolver"),
isSelected: "code_resolver" === selectedAgentId,
isInteractive:
- extendedAgents?.find((a) => a.name === "code_resolver")
- ?.status !== "skipped",
+ Boolean(isEditMode) ||
+ !isAgentWaitingOrSkipped(extendedAgents, "code_resolver"),
+ isDisabled:
+ !isEditMode &&
+ isAgentWaitingOrSkipped(extendedAgents, "code_resolver"),
isEditMode,
onAddMCPServer,
onEditMCPServer,
diff --git a/src/components/RecentActivity/Badge/index.tsx b/src/components/RecentActivity/Badge/index.tsx
index 3d47e54a2..29787bb73 100644
--- a/src/components/RecentActivity/Badge/index.tsx
+++ b/src/components/RecentActivity/Badge/index.tsx
@@ -1,4 +1,4 @@
-import { memo } from "react";
+import React from "react";
import * as s from "./styles";
import type { BadgeProps } from "./types";
@@ -6,4 +6,4 @@ const BadgeComponent = ({ size = "small", className }: BadgeProps) => (
);
-export const Badge = memo(BadgeComponent);
+export const Badge = React.memo(BadgeComponent);
diff --git a/src/components/common/Badge/index.tsx b/src/components/common/Badge/index.tsx
index 36309fba3..120538a25 100644
--- a/src/components/common/Badge/index.tsx
+++ b/src/components/common/Badge/index.tsx
@@ -1,4 +1,4 @@
-import { memo } from "react";
+import React from "react";
import * as s from "./styles";
import type { BadgeProps } from "./types";
@@ -8,4 +8,4 @@ const BadgeComponent = ({ customStyles }: BadgeProps) => (
);
-export const Badge = memo(BadgeComponent);
+export const Badge = React.memo(BadgeComponent);
diff --git a/src/components/common/Loader/index.tsx b/src/components/common/Loader/index.tsx
index cb2063e68..68e8a5c19 100644
--- a/src/components/common/Loader/index.tsx
+++ b/src/components/common/Loader/index.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react/jsx-curly-brace-presence */
-import { memo } from "react";
+import React from "react";
import { CircleLoader } from "../CircleLoader";
import type { LoaderProps } from "./types";
@@ -365,4 +365,4 @@ const LoaderComponent = ({ size = 20, status, themeKind }: LoaderProps) => {
}
};
-export const Loader = memo(LoaderComponent);
+export const Loader = React.memo(LoaderComponent);