Skip to content

feat: SSE console updates from Korrel8r#231

Open
alanconway wants to merge 2 commits intoopenshift:mainfrom
alanconway:mcp-korrel8r
Open

feat: SSE console updates from Korrel8r#231
alanconway wants to merge 2 commits intoopenshift:mainfrom
alanconway:mcp-korrel8r

Conversation

@alanconway
Copy link
Copy Markdown
Contributor

@alanconway alanconway commented Apr 24, 2026

Allows console to send state and receive requests to/from an AI agent.
Console uses korrel8r REST API, agent uses MCP API.

  • Menu with AI icon allows enable/disable, shows connection status.
  • Subscribe to console updates from AI agent via korrel8r.
  • Always show troubleshooting panel, even if korrel8r is unavailable - it may become available.

Summary by CodeRabbit

  • New Features

    • Introduced AI Agent navigation to automatically assist with console operations.
    • Added AI Agent settings toggle with connection status indicators (Connected, Connecting, Off).
  • API Changes

    • Updated REST endpoint contracts for console and configuration operations.
    • Deprecated the /graphs/neighbours endpoint in favor of /graphs/neighbors.

@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented Apr 24, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: alanconway

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Apr 24, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Walkthrough

This PR introduces AI Agent navigation support for Korrel8r by updating the OpenAPI contract to formalize console control (deprecating the old endpoint spelling, restructuring Console payloads, and adding streaming event support), implementing client-side agent communication with retry logic, wiring agent state through Redux, and adding UI components for toggling the agent while maintaining backward compatibility.

Changes

AI Agent Integration & OpenAPI Contract Alignment

Layer / File(s) Summary
OpenAPI Contract Changes
korrel8r/korrel8r-openapi.yaml
Rest contract formalized with session isolation semantics, PUT /config and PUT /console now return JSON payloads, GET /console/events uses SSE with JSON-encoded Console objects, POST /graphs/neighbours deprecated, Constraint.timeout removed, Console restructured with view and search fields, Node extended with queries, count, result, and additional metadata tags on schemas.
Type Definitions & Generated Client
web/src/korrel8r/types.ts, web/src/korrel8r/client/index.ts, web/src/__tests__/types.spec.ts
Constraint class removes timeoutNS and timeout() method, fromAPI() returns Constraint | undefined, serializes only start/end/limit; generated types export singular response types (ConsoleEventsResponse, SetConfigResponse, SetConsoleResponse), remove Duration export; test case updated to remove timeout coverage.
Client Streaming & Retry Helpers
web/src/korrel8r-client.ts
Graph client call switched to graphNeighbors spelling; three new exported helpers added: setConsole() returns a cancellable promise via AbortController, consoleEvents() streams events with exponential backoff retry and onConnect/onEvent/onError callbacks, retryWithBackoff() wraps generic retry logic.
Redux State & Actions
web/src/redux-actions.ts, web/src/redux-reducers.ts
ActionType enum adds SetAgentConnected and SetAgentEnabled, corresponding action creators exported; reducer initial state adds agentEnabled and agentConnected (both false), switch adds handlers for the two new actions; apiSearch() and reduxSearch() helpers added to convert between internal Search and API shapes.
Agent Communication Hook
web/src/hooks/useKorrel8r.ts
Hook rewritten from a simple reachability check to a configurable default export accepting { minDelay?, maxDelay? } for retry backoff; now manages agent communication: sends setConsole updates when agentEnabled is true, subscribes to consoleEvents to update view/search via Redux and navigate, marks agent connection state, with error handling (canceling on abort, logging other errors).
Supporting Hooks Integration
web/src/hooks/usePopover.tsx, web/src/hooks/useTroubleshootingPanel.tsx, web/src/hooks/useLocationQuery.ts
usePopover calls useKorrel8r() on mount to initialize agent; useTroubleshootingPanel drops useKorrel8r usage, simplifies action logic to dev-perspective check only, removes isKorrel8rReachable dependency; useLocationQuery adds deduped error warning via lastError state to avoid spam on repeated failures.
Agent UI Components & Panel Integration
web/src/components/AIExperienceIcon.tsx, web/src/components/AgentMenu.tsx, web/src/components/Korrel8rPanel.tsx
New AIExperienceIcon SVG component renders icon with currentColor fill; new AgentMenu dropdown toggle dispatches setAgentEnabled() and displays localized connection status ("Connected"/"Connecting…"/"Off") with color feedback; Korrel8rPanel toolbar inserts <AgentMenu /> and refactors refresh button from Tooltip-wrapped link to plain icon-only SyncIcon button.
Localization & Strings
web/locales/en/plugin__troubleshooting-panel-console-plugin.json
Adds new keys for "Agent Navigation", "AI Agent settings", state labels ("Connected", "Connecting...", "Off"), and agent enablement strings; removes "Refresh the graph by re-running the current search." key.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 11 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (11 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: SSE console updates from Korrel8r' directly and accurately summarizes the main change—enabling server-sent event streaming of console state from the Korrel8r service—which aligns with the PR's core objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed This check applies to Ginkgo test naming. The PR contains no Ginkgo tests—only TypeScript/JavaScript, JSON, and YAML files. Check is not applicable.
Test Structure And Quality ✅ Passed Custom check for Ginkgo test quality not applicable. PR contains 0 Ginkgo test files (no Go test modifications). All 15 modified files are TypeScript/React/YAML/JSON.
Microshift Test Compatibility ✅ Passed No Ginkgo e2e tests are added. PR contains only TypeScript/React code and locale changes. Custom check not applicable.
Single Node Openshift (Sno) Test Compatibility ✅ Passed No Ginkgo e2e tests added in this PR. Repository uses Jest (TypeScript) and standard Go testing. Check is not applicable to this console plugin project.
Topology-Aware Scheduling Compatibility ✅ Passed PR modifies only console plugin UI code (React components, hooks, OpenAPI spec) with no deployment manifests, operator code, or controllers. Check not applicable.
Ote Binary Stdout Contract ✅ Passed PR modifies only TypeScript/React/YAML/JSON files. No Go files are modified. Existing Go backend code (cmd/plugin-backend.go, pkg/server.go) uses logrus with stderr output, no stdout violations.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed Check not applicable. PR modifies TypeScript/React console plugin, not Go/Ginkgo tests. No new Ginkgo e2e tests present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
web/src/components/Korrel8rPanel.tsx (2)

63-63: Type annotation may not reflect actual Redux state.

The agentError selector could return undefined when the state key hasn't been set. Consider updating the type to reflect this:

-  const agentError: string = useSelector((state: State) => state.plugins?.tp?.get('agentError'));
+  const agentError: string | undefined = useSelector((state: State) => state.plugins?.tp?.get('agentError'));

The current falsy checks (line 184, 190) handle this safely at runtime, so this is just a type accuracy improvement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Korrel8rPanel.tsx` at line 63, The agentError selector is
typed too narrowly as string even though state.plugins?.tp?.get('agentError')
can be undefined; update the type on the agentError constant (the useSelector
call that reads state.plugins?.tp?.get('agentError')) to reflect string |
undefined (or the appropriate union) so the TypeScript type matches runtime
behavior and the existing falsy checks around agentError remain valid.

180-196: Good addition of connection status indicator.

The status button properly reflects agent connectivity using agentError from Redux. The use of PatternFly CSS variables for icon colors is appropriate.

Minor i18n concern: String concatenation on line 185 bypasses proper interpolation. Consider using interpolation for better localization support:

-                  ? t('Correlation service error: ') + agentError
+                  ? t('Correlation service error: {{error}}', { error: agentError })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Korrel8rPanel.tsx` around lines 180 - 196, Replace the
string concatenation when building the Tooltip content with a localized
interpolation using the t function so translations can reorder/format the agent
error; specifically, update the Tooltip content expression that currently uses
agentError and t('Correlation service error: ') to call t with a placeholder
(e.g., t('Correlation service error: {{error}}', { error: agentError })) or the
project's established interpolation pattern; change only the Tooltip content
construction near the agentError usage so Tooltip, agentError, and the t(...)
call are used with interpolation instead of simple string concatenation.
web/src/hooks/useKorrel8r.ts (1)

53-58: Ref pattern for stable SSE callbacks.

Using a ref to hold navigateToQuery and search without triggering effect re-runs is a valid pattern. Consider adding a brief comment explaining why the effect at lines 55-58 has no dependency array (intentionally updates ref on every render):

   // Ref for values used in SSE callback that shouldn't trigger reconnection.
   const ref = React.useRef({ navigateToQuery, search });
+  // Intentionally no deps - update ref on every render to keep callbacks current
   React.useEffect(() => {
     ref.current.navigateToQuery = navigateToQuery;
     ref.current.search = search;
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/hooks/useKorrel8r.ts` around lines 53 - 58, Add a short inline
comment above the ref and the useEffect explaining that ref (created via
React.useRef) holds navigateToQuery and search so SSE callbacks remain stable
and the empty dependency omission is intentional to update ref on every render;
also add an eslint-disable-next-line react-hooks/exhaustive-deps on the effect
if the linter complains to make the intent explicit.
web/src/redux-actions.ts (1)

48-54: Consider handling missing queryStr in apiSearch.

If search.queryStr is undefined or empty, the function creates queries: [undefined] or queries: [''], which may cause issues downstream in the API call. You might want to validate or filter:

 export const apiSearch = (search: Search): api.Search => {
-  const start: api.Start = { queries: [search.queryStr] };
+  const start: api.Start = { queries: search.queryStr ? [search.queryStr] : [] };
   if (search.searchType === SearchType.Goal) {
     return { goals: { start, goals: [search.goal] } };
   }
   return { neighbors: { start, depth: search.depth } };
 };

Alternatively, this may already be guarded at call sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/redux-actions.ts` around lines 48 - 54, In apiSearch, validate that
search.queryStr is a non-empty string before building api.Start; if it is falsy
or empty, throw a descriptive error (e.g., "apiSearch: missing queryStr") or
otherwise return a safe api.Search variant; update the apiSearch function to
check search.queryStr (used when constructing api.Start.queries) and either
filter it out / use an empty queries array or throw so downstream API calls
never receive [undefined] or [''].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/src/components/Korrel8rPanel.tsx`:
- Line 63: The agentError selector is typed too narrowly as string even though
state.plugins?.tp?.get('agentError') can be undefined; update the type on the
agentError constant (the useSelector call that reads
state.plugins?.tp?.get('agentError')) to reflect string | undefined (or the
appropriate union) so the TypeScript type matches runtime behavior and the
existing falsy checks around agentError remain valid.
- Around line 180-196: Replace the string concatenation when building the
Tooltip content with a localized interpolation using the t function so
translations can reorder/format the agent error; specifically, update the
Tooltip content expression that currently uses agentError and t('Correlation
service error: ') to call t with a placeholder (e.g., t('Correlation service
error: {{error}}', { error: agentError })) or the project's established
interpolation pattern; change only the Tooltip content construction near the
agentError usage so Tooltip, agentError, and the t(...) call are used with
interpolation instead of simple string concatenation.

In `@web/src/hooks/useKorrel8r.ts`:
- Around line 53-58: Add a short inline comment above the ref and the useEffect
explaining that ref (created via React.useRef) holds navigateToQuery and search
so SSE callbacks remain stable and the empty dependency omission is intentional
to update ref on every render; also add an eslint-disable-next-line
react-hooks/exhaustive-deps on the effect if the linter complains to make the
intent explicit.

In `@web/src/redux-actions.ts`:
- Around line 48-54: In apiSearch, validate that search.queryStr is a non-empty
string before building api.Start; if it is falsy or empty, throw a descriptive
error (e.g., "apiSearch: missing queryStr") or otherwise return a safe
api.Search variant; update the apiSearch function to check search.queryStr (used
when constructing api.Start.queries) and either filter it out / use an empty
queries array or throw so downstream API calls never receive [undefined] or
[''].

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 8915985c-7290-4b78-b3a8-d213202392df

📥 Commits

Reviewing files that changed from the base of the PR and between 4440656 and 72ea85e.

⛔ Files ignored due to path filters (2)
  • web/src/korrel8r/client/sdk.gen.ts is excluded by !**/*.gen.*
  • web/src/korrel8r/client/types.gen.ts is excluded by !**/*.gen.*
📒 Files selected for processing (14)
  • korrel8r/korrel8r-openapi.yaml
  • web/locales/en/plugin__troubleshooting-panel-console-plugin.json
  • web/package.json
  • web/src/__tests__/types.spec.ts
  • web/src/components/Korrel8rPanel.tsx
  • web/src/hooks/useKorrel8r.ts
  • web/src/hooks/useNavigateToQuery.ts
  • web/src/hooks/usePopover.tsx
  • web/src/hooks/useTroubleshootingPanel.tsx
  • web/src/korrel8r-client.ts
  • web/src/korrel8r/client/index.ts
  • web/src/korrel8r/types.ts
  • web/src/redux-actions.ts
  • web/src/redux-reducers.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/hooks/useKorrel8r.ts`:
- Around line 36-40: The current body always includes search: apiSearch(search)
even when the panel is closed or search.queryStr is empty, causing blank/stale
queries to be sent; change the body construction so that search is only added
when isOpen && search?.queryStr is truthy (keep view always), e.g., build body =
{ view, ...(isOpen && search?.queryStr ? { search: apiSearch(search) } : {}) }
before calling retryWithBackoff; reference symbols: agentAllowed, view, isOpen,
search, apiSearch, retryWithBackoff.
- Around line 69-83: The consoleEvents callback currently calls
Query.parse(event.view) and reduxSearch(event.search) directly which can throw
and bubble up to consoleEvents; wrap each external-parse step in a local
try/catch and validate results before applying (e.g., call Query.parse inside
try/catch and only call navigateToQueryRef.current with a valid Query, and call
reduxSearch inside try/catch and only
dispatch(setSearch(...))/dispatch(openTP()) when reduxSearch returns a valid
object), and on parse/validation errors log the malformed payload (using
console.debug or process logger) and return early so the SSE session is not
closed by an exception.

In `@web/src/redux-actions.ts`:
- Around line 60-76: reduxSearch currently lets search.neighbors overwrite
search.goals; instead detect how many top-level variants are set (e.g.,
search.goals and search.neighbors) and only build a Search when exactly one
variant is present. In the reduxSearch function check presence of each variant
(search.goals, search.neighbors), if count !== 1 return undefined; if exactly
one, construct the Search from that branch (use queryStr from
start?.queries?.[0], set searchType and variant-specific fields like goal or
depth) and return it. Ensure the result variable is typed to allow undefined and
remove the last-one-wins logic.

In `@web/src/redux-reducers.ts`:
- Around line 18-23: The initial state currently sets agentAllowed: true in the
reducer's ImmutableMap (the default store returned by the reducer), which
opt-ins users to agent mirroring; change the default to false by updating the
return value that creates the state (the ImmutableMap containing isOpen, search,
agentAllowed, agentConnected) so agentAllowed is initialized to false; also scan
for any tests or initialization logic that rely on the previous default and
update them accordingly (look for uses of agentAllowed in
reducers/selectors/tests).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 48bf6374-5fcb-4573-8b02-5c7289ebf447

📥 Commits

Reviewing files that changed from the base of the PR and between 72ea85e and 2e5c434.

⛔ Files ignored due to path filters (3)
  • web/package-lock.json is excluded by !**/package-lock.json
  • web/src/korrel8r/client/sdk.gen.ts is excluded by !**/*.gen.*
  • web/src/korrel8r/client/types.gen.ts is excluded by !**/*.gen.*
📒 Files selected for processing (16)
  • korrel8r/korrel8r-openapi.yaml
  • web/locales/en/plugin__troubleshooting-panel-console-plugin.json
  • web/package.json
  • web/src/__tests__/types.spec.ts
  • web/src/components/Korrel8rPanel.tsx
  • web/src/components/Popover.tsx
  • web/src/hooks/useKorrel8r.ts
  • web/src/hooks/useLocationQuery.ts
  • web/src/hooks/useNavigateToQuery.ts
  • web/src/hooks/usePopover.tsx
  • web/src/hooks/useTroubleshootingPanel.tsx
  • web/src/korrel8r-client.ts
  • web/src/korrel8r/client/index.ts
  • web/src/korrel8r/types.ts
  • web/src/redux-actions.ts
  • web/src/redux-reducers.ts
✅ Files skipped from review due to trivial changes (6)
  • web/package.json
  • web/src/hooks/useTroubleshootingPanel.tsx
  • web/src/hooks/useNavigateToQuery.ts
  • web/src/tests/types.spec.ts
  • web/locales/en/plugin__troubleshooting-panel-console-plugin.json
  • web/src/korrel8r/client/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • web/src/components/Korrel8rPanel.tsx
  • web/src/korrel8r-client.ts
  • web/src/hooks/usePopover.tsx

Comment thread web/src/hooks/useKorrel8r.ts Outdated
Comment thread web/src/hooks/useKorrel8r.ts
Comment thread web/src/redux-actions.ts
Comment thread web/src/redux-reducers.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
web/src/hooks/useKorrel8r.ts (1)

36-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only send search when it matches the visible panel state.

Line 39 still serializes search whenever view exists, so a closed panel can resend stale search state and an empty query can still be pushed upstream. This should stay conditional on the panel being open and search.queryStr being non-empty.

Suggested fix
-    const body = { view, search: apiSearch(search) };
+    const body = {
+      view,
+      ...(isOpen && search?.queryStr ? { search: apiSearch(search) } : {}),
+    };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/hooks/useKorrel8r.ts` around lines 36 - 40, The current logic always
includes search in the request body when view exists, allowing a closed panel or
empty query to resend stale/empty search state; update the body construction in
useKorrel8r so that search is only added when the panel is open (isOpen true)
and search?.queryStr is non-empty — e.g., build body = { view, ...(isOpen &&
search?.queryStr ? { search: apiSearch(search) } : {}) } before calling
retryWithBackoff; reference the body variable, apiSearch(search), and the
isOpen/search?.queryStr checks to locate where to change the serialization.
🧹 Nitpick comments (1)
web/src/hooks/useTroubleshootingPanel.tsx (1)

39-41: 💤 Low value

Redundant dependency in useEffect.

open is already included in getActions's dependency array (line 35), so including it again in the useEffect dependency array is unnecessary. The effect will re-run when getActions changes, which already happens when open changes.

Suggested simplification
   useEffect(() => {
     setActions(getActions());
-  }, [open, getActions]);
+  }, [getActions]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/hooks/useTroubleshootingPanel.tsx` around lines 39 - 41, The
useEffect that calls setActions(getActions()) has an unnecessary dependency:
open is already included in the dependency array of getActions, so remove open
from useEffect's dependency list and keep only getActions; i.e., update the
effect that references setActions and getActions so it depends solely on
getActions to avoid redundant re-renders.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/components/Korrel8rPanel.tsx`:
- Around line 166-170: The Button currently uses the accessibility-only prop
isAriaDisabled={!search?.queryStr} which does not block clicks and allows
onClick={() => refetch()} to fire; update the Button props to use
isDisabled={!search?.queryStr} instead of isAriaDisabled so the control is truly
non-interactive when there is no search query, preventing unintended refetch()
calls (locate the Button component rendering around Korrel8rPanel where onClick
calls refetch).

In `@web/src/korrel8r-client.ts`:
- Around line 178-189: The code currently calls onConnect() before ensuring the
Korrel8r stream is valid, which can report a connection when an HTTP error makes
result.stream unusable; fix by calling sdkConsoleEvents with throwOnError: true
(e.g., await sdkConsoleEvents({ client: korrel8rClient, throwOnError: true }))
so errors are thrown instead of returning an invalid result, and only invoke
onConnect() after verifying result.stream is present/iterable (or after
successfully receiving the first event from result.stream while still respecting
controller.signal) to avoid transient connected-state reporting; update calls
referencing createClient, sdkConsoleEvents, onConnect, result.stream, and
controller.signal accordingly.

In `@web/src/korrel8r/types.ts`:
- Around line 65-70: The mapping in Constraint.static fromAPI currently treats 0
as falsy and converts {limit: 0} to undefined; change the assignment for the
limit field to use the nullish coalescing operator (e.g., limit:
constraint.limit ?? undefined) so that 0 is preserved, and apply the same fix in
the Constraint.prototype.toAPI (or toAPI) mapping to ensure round-tripping
preserves explicit 0 values; update the constructors/users of Constraint if they
rely on the previous falsy behavior.

In `@web/src/redux-actions.ts`:
- Line 61: The variable declaration "let result: Search = undefined;" assigns
undefined to a non-optional Search type; change the type annotation for the
variable named result to allow undefined (e.g., let result: Search | undefined)
or initialize it with a valid Search value so the assignment matches the
declared type; update the declaration of result in redux-actions.ts accordingly.

---

Duplicate comments:
In `@web/src/hooks/useKorrel8r.ts`:
- Around line 36-40: The current logic always includes search in the request
body when view exists, allowing a closed panel or empty query to resend
stale/empty search state; update the body construction in useKorrel8r so that
search is only added when the panel is open (isOpen true) and search?.queryStr
is non-empty — e.g., build body = { view, ...(isOpen && search?.queryStr ? {
search: apiSearch(search) } : {}) } before calling retryWithBackoff; reference
the body variable, apiSearch(search), and the isOpen/search?.queryStr checks to
locate where to change the serialization.

---

Nitpick comments:
In `@web/src/hooks/useTroubleshootingPanel.tsx`:
- Around line 39-41: The useEffect that calls setActions(getActions()) has an
unnecessary dependency: open is already included in the dependency array of
getActions, so remove open from useEffect's dependency list and keep only
getActions; i.e., update the effect that references setActions and getActions so
it depends solely on getActions to avoid redundant re-renders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 3a321b8d-7a63-4640-be79-b61186c4e501

📥 Commits

Reviewing files that changed from the base of the PR and between 2e5c434 and 370252e.

⛔ Files ignored due to path filters (3)
  • web/package-lock.json is excluded by !**/package-lock.json
  • web/src/korrel8r/client/sdk.gen.ts is excluded by !**/*.gen.*
  • web/src/korrel8r/client/types.gen.ts is excluded by !**/*.gen.*
📒 Files selected for processing (16)
  • korrel8r/korrel8r-openapi.yaml
  • web/locales/en/plugin__troubleshooting-panel-console-plugin.json
  • web/package.json
  • web/src/__tests__/types.spec.ts
  • web/src/components/Korrel8rPanel.tsx
  • web/src/components/Popover.tsx
  • web/src/hooks/useKorrel8r.ts
  • web/src/hooks/useLocationQuery.ts
  • web/src/hooks/useNavigateToQuery.ts
  • web/src/hooks/usePopover.tsx
  • web/src/hooks/useTroubleshootingPanel.tsx
  • web/src/korrel8r-client.ts
  • web/src/korrel8r/client/index.ts
  • web/src/korrel8r/types.ts
  • web/src/redux-actions.ts
  • web/src/redux-reducers.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/package.json
  • web/src/hooks/useNavigateToQuery.ts

Comment thread web/src/components/Korrel8rPanel.tsx
Comment thread web/src/korrel8r-client.ts
Comment thread web/src/korrel8r/types.ts
Comment thread web/src/redux-actions.ts Outdated
@fkargbo
Copy link
Copy Markdown

fkargbo commented May 6, 2026

image I would go with this icon as per Branding guidelines for all things AI related. I left you some comments in Slack.

@alanconway alanconway marked this pull request as draft May 6, 2026 14:10
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label May 6, 2026
@alanconway
Copy link
Copy Markdown
Contributor Author

I'm changing the AI stuff again to follow the explicit "ask first" pattern, with an option to "don't ask again". Simplifies the UI since the user is presented with a question automatically the first time we try to interact with the agent.

Update to latest korrel8r API spec, minor changes to types.
Copy link
Copy Markdown
Contributor

@PeterYurkovich PeterYurkovich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the long time to review

As a more general question I am wondering if we should add some AI enabled flag onto the UIPlugin CR for the troubleshooting panel so a cluster admin can dictate it's usage (this would require changes in obo and adding a feature flag for the troubleshooting panel)

Comment thread web/package.json Outdated
Comment thread web/package.json Outdated
Comment thread web/src/redux-reducers.ts
Comment thread web/src/redux-actions.ts
Comment thread web/src/hooks/useTroubleshootingPanel.tsx
cancel: () => void;
}

export const setConsole = (body: Console): CancellablePromise<void> => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SSE code feels quite divergent from the REST functions. Could we split into a separate file?

Comment thread web/src/hooks/useKorrel8r.ts
Comment thread web/src/hooks/useKorrel8r.ts
Allows console to send state and receive requests to/from an AI agent.
Console uses korrel8r REST API, agent uses MCP API.

- Menu with AI icon allows enable/disable, shows connection status.
- Subscribe to console updates from AI agent via korrel8r.
- Always show troubleshooting panel, even if korrel8r is unavailable - it may become available.
@alanconway alanconway marked this pull request as ready for review May 7, 2026 13:04
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label May 7, 2026
@alanconway alanconway requested a review from PeterYurkovich May 7, 2026 13:04
@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented May 7, 2026

@alanconway: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
web/src/korrel8r-client.ts (1)

178-186: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only report “connected” after the stream is actually usable.

This path still omits error-throwing and calls onConnect() before proving result.stream exists, so HTTP failures can briefly flip the UI to connected and then immediately error. Move onConnect() behind a successful stream check and make the SDK call fail hard on non-2xx responses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/korrel8r-client.ts` around lines 178 - 186, The code calls
onConnect() before ensuring the SDK returned a usable stream and doesn’t surface
non-2xx HTTP failures; update the logic around createClient/sdkConsoleEvents so
you first call sdkConsoleEvents with the requestWrapper that throws on non-2xx
responses (or check the response status) and verify that result.stream is an
async iterable before proceeding, only invoking onConnect() after confirming
result.stream exists and is usable; adjust requestWrapper or the
sdkConsoleEvents call to "fail hard" on non-2xx responses and move the
onConnect() call to after the successful stream check (references: createClient,
requestWrapper, sdkConsoleEvents, result.stream, onConnect).
web/src/korrel8r/types.ts (1)

65-78: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve an explicit limit: 0 when converting constraints.

|| undefined still drops 0 on both read and write, so { limit: 0 } does not round-trip. This needs ?? undefined in both places.

Suggested fix
   static fromAPI(constraint: api.Constraint): Constraint | undefined {
     if (!constraint) return undefined;
     return new Constraint({
       start: parseDate(constraint.start),
       end: parseDate(constraint.end),
-      limit: constraint?.limit || undefined,
+      limit: constraint?.limit ?? undefined,
     });
   }

   toAPI(): api.Constraint {
     return {
       start: this?.start?.toISOString() || undefined,
       end: this?.end?.toISOString() || undefined,
-      limit: this?.limit || undefined,
+      limit: this?.limit ?? undefined,
     };
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/korrel8r/types.ts` around lines 65 - 78, The conversion loses an
explicit limit: 0 because the code uses the || operator which treats 0 as falsy;
update both Constraint.fromAPI and Constraint.toAPI to use the nullish
coalescing operator (?? undefined) when mapping limit so that 0 is preserved
(i.e., replace occurrences of constraint?.limit || undefined and this?.limit ||
undefined with constraint?.limit ?? undefined and this?.limit ?? undefined
respectively).
🧹 Nitpick comments (5)
web/src/hooks/useLocationQuery.ts (1)

46-58: ⚡ Quick win

Move side effects out of the render phase.

console.warn (line 55) and the related setLastError calls execute during render. In React Strict Mode, render functions are invoked twice, which can cause duplicate warnings despite the deduplication logic. Moving the logging and error-state sync into a useEffect aligns with React's intended data flow and avoids Strict Mode quirks.

♻️ Suggested refactor using useEffect
 export const useLocationQuery = (): Query | undefined => {
   const domains = useDomains();
   const location = useBrowserLocation();
   const [lastError, setLastError] = useState('');
-  try {
-    const query = domains.linkToQuery(new URIRef(location.pathname + location.search));
-    if (lastError) setLastError('');
-    return query;
-  } catch (err) {
-    const errStr = String(err);
-    if (errStr !== lastError) {
-      // eslint-disable-next-line no-console
-      console.warn(`korrel8r useLocationQuery: ${errStr}`);
-      setLastError(errStr);
-    }
-  }
+
+  let query: Query | undefined;
+  let currentError = '';
+  try {
+    query = domains.linkToQuery(new URIRef(location.pathname + location.search));
+  } catch (err) {
+    currentError = String(err);
+  }
+
+  useEffect(() => {
+    if (currentError !== lastError) {
+      if (currentError) {
+        // eslint-disable-next-line no-console
+        console.warn(`korrel8r useLocationQuery: ${currentError}`);
+      }
+      setLastError(currentError);
+    }
+  }, [currentError, lastError]);
+
+  return query;
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/hooks/useLocationQuery.ts` around lines 46 - 58, The current
try/catch in useLocationQuery runs console.warn and setLastError during render
(variables: lastError, setLastError, domains.linkToQuery, URIRef), so move those
side effects into a useEffect: have the render phase only compute/return the
query (or null/undefined on error) and capture the error value locally, then in
a useEffect dependent on that error value update setLastError and call
console.warn (or process the error) so logging and state updates run after
render and avoid Strict Mode double-invocation issues.
web/src/__tests__/types.spec.ts (1)

52-56: ⚡ Quick win

Add a limit: 0 round-trip case here.

This matrix now covers limit, but it still misses the falsy boundary value that currently regresses in Constraint.fromAPI()/toAPI(). Adding { clientC: { limit: 0 }, typesC: { limit: 0 } } will lock in the fix once the serializer is corrected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/__tests__/types.spec.ts` around lines 52 - 56, Add a test case to the
it.each matrix in types.spec.ts to include the falsy boundary value by adding an
entry for limit: 0 so the round-trip via Constraint.fromAPI() and toAPI() is
exercised; specifically add a case object equivalent to { clientC: { limit: 0 },
typesC: { limit: 0 } } alongside the existing limit cases to ensure the
serializer correctly preserves zero.
korrel8r/korrel8r-openapi.yaml (2)

339-342: 💤 Low value

Consider specifying empty object schema for 200 response.

The 200 response for PUT /console returns type: object without properties. If the response is always empty, consider adding additionalProperties: false to make the contract explicit. If it may contain data in the future, the current permissive schema is acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@korrel8r/korrel8r-openapi.yaml` around lines 339 - 342, The 200 response
schema for the PUT /console operation currently uses a permissive "type: object"
with no properties; to make an explicit empty response contract, update the
response schema for the PUT /console 200 to include "additionalProperties:
false" (keeping type: object) so the spec clearly disallows any properties, or
alternatively add explicit properties if you expect fields later; locate the 200
response block in the OpenAPI spec where "type: object" is defined and add the
additionalProperties: false key there.

688-706: ⚡ Quick win

Mutual exclusivity of goals and neighbors not enforced in schema.

The description states "Set exactly one of 'goals' ... or 'neighbors'" but the schema allows both, neither, or either. Consider using oneOf to enforce this constraint at the schema level, which would enable client-side validation and clearer generated types.

♻️ Suggested schema constraint
     Search:
       description: >
         Correlation search parameters.
         Set exactly one of 'goals' (targeted search to specific classes)
         or 'neighbors' (open-ended exploration to a depth).
       type: object
+      oneOf:
+        - required: [goals]
+          not:
+            required: [neighbors]
+        - required: [neighbors]
+          not:
+            required: [goals]
       properties:
         goals:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@korrel8r/korrel8r-openapi.yaml` around lines 688 - 706, The Search schema
currently allows both or neither of goals and neighbors although its description
requires exactly one; update the OpenAPI component for Search to enforce mutual
exclusivity by replacing the separate properties block with a oneOf that
references the existing Goals and Neighbors schemas (using the same $ref
targets), e.g. provide one schema option with goals: { $ref:
"#/components/schemas/Goals" } and another with neighbors: { $ref:
"#/components/schemas/Neighbors" } so generated clients and validators will
require exactly one of goals or neighbors; keep the existing descriptions and
x-oapi-codegen-extra-tags on the referenced schemas.
web/src/hooks/useKorrel8r.ts (1)

68-78: 💤 Low value

Variable shadowing: inner search shadows outer scope.

Line 73 declares const search which shadows the search variable from line 29. While functionally correct here, this can cause confusion during maintenance. Consider renaming to nextSearch or similar.

Also, the async keyword on line 68 is unnecessary since the callback doesn't use await.

♻️ Suggested cleanup
     return consoleEvents(
-      async (event) => {
+      (event) => {
         if (event.view) {
           navigateToQueryRef.current(Query.parse(event.view), null);
         }
         if (event.search) {
-          const search = reduxSearch(event.search);
-          if (search) {
-            dispatch(setSearch(search));
+          const nextSearch = reduxSearch(event.search);
+          if (nextSearch) {
+            dispatch(setSearch(nextSearch));
             dispatch(openTP());
           }
         }
       },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/hooks/useKorrel8r.ts` around lines 68 - 78, Rename the inner const
search to avoid shadowing the outer search (e.g., call it nextSearch) and update
its usages (reduxSearch -> nextSearch result) in the event callback in
useKorrel8r.ts; also remove the unnecessary async keyword from the anonymous
event handler since it doesn't use await. Locate the handler that calls
navigateToQueryRef.current(Query.parse(event.view)), reduxSearch(event.search),
dispatch(setSearch(...)) and dispatch(openTP()) and apply the renaming and async
removal there.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/korrel8r-client.ts`:
- Around line 193-194: The exponential backoff wait using await new Promise((r)
=> setTimeout(r, delay)) is not abortable and lets cancel() block until the
timeout elapses; change both retry loops to use an abortable delay that races
setTimeout against an AbortSignal (or rejects when the provided signal is
aborted) so the loop can exit immediately on cancellation, update the cancel()
path to call abort() on that signal (or otherwise resolve the delay promise),
and replace the two occurrences that manipulate delay and maxDelay (the backoff
logic around delay/maxDelay) with calls to this abortableDelay helper so
reconnect timers don't fire after unmount/disable.

---

Duplicate comments:
In `@web/src/korrel8r-client.ts`:
- Around line 178-186: The code calls onConnect() before ensuring the SDK
returned a usable stream and doesn’t surface non-2xx HTTP failures; update the
logic around createClient/sdkConsoleEvents so you first call sdkConsoleEvents
with the requestWrapper that throws on non-2xx responses (or check the response
status) and verify that result.stream is an async iterable before proceeding,
only invoking onConnect() after confirming result.stream exists and is usable;
adjust requestWrapper or the sdkConsoleEvents call to "fail hard" on non-2xx
responses and move the onConnect() call to after the successful stream check
(references: createClient, requestWrapper, sdkConsoleEvents, result.stream,
onConnect).

In `@web/src/korrel8r/types.ts`:
- Around line 65-78: The conversion loses an explicit limit: 0 because the code
uses the || operator which treats 0 as falsy; update both Constraint.fromAPI and
Constraint.toAPI to use the nullish coalescing operator (?? undefined) when
mapping limit so that 0 is preserved (i.e., replace occurrences of
constraint?.limit || undefined and this?.limit || undefined with
constraint?.limit ?? undefined and this?.limit ?? undefined respectively).

---

Nitpick comments:
In `@korrel8r/korrel8r-openapi.yaml`:
- Around line 339-342: The 200 response schema for the PUT /console operation
currently uses a permissive "type: object" with no properties; to make an
explicit empty response contract, update the response schema for the PUT
/console 200 to include "additionalProperties: false" (keeping type: object) so
the spec clearly disallows any properties, or alternatively add explicit
properties if you expect fields later; locate the 200 response block in the
OpenAPI spec where "type: object" is defined and add the additionalProperties:
false key there.
- Around line 688-706: The Search schema currently allows both or neither of
goals and neighbors although its description requires exactly one; update the
OpenAPI component for Search to enforce mutual exclusivity by replacing the
separate properties block with a oneOf that references the existing Goals and
Neighbors schemas (using the same $ref targets), e.g. provide one schema option
with goals: { $ref: "#/components/schemas/Goals" } and another with neighbors: {
$ref: "#/components/schemas/Neighbors" } so generated clients and validators
will require exactly one of goals or neighbors; keep the existing descriptions
and x-oapi-codegen-extra-tags on the referenced schemas.

In `@web/src/__tests__/types.spec.ts`:
- Around line 52-56: Add a test case to the it.each matrix in types.spec.ts to
include the falsy boundary value by adding an entry for limit: 0 so the
round-trip via Constraint.fromAPI() and toAPI() is exercised; specifically add a
case object equivalent to { clientC: { limit: 0 }, typesC: { limit: 0 } }
alongside the existing limit cases to ensure the serializer correctly preserves
zero.

In `@web/src/hooks/useKorrel8r.ts`:
- Around line 68-78: Rename the inner const search to avoid shadowing the outer
search (e.g., call it nextSearch) and update its usages (reduxSearch ->
nextSearch result) in the event callback in useKorrel8r.ts; also remove the
unnecessary async keyword from the anonymous event handler since it doesn't use
await. Locate the handler that calls
navigateToQueryRef.current(Query.parse(event.view)), reduxSearch(event.search),
dispatch(setSearch(...)) and dispatch(openTP()) and apply the renaming and async
removal there.

In `@web/src/hooks/useLocationQuery.ts`:
- Around line 46-58: The current try/catch in useLocationQuery runs console.warn
and setLastError during render (variables: lastError, setLastError,
domains.linkToQuery, URIRef), so move those side effects into a useEffect: have
the render phase only compute/return the query (or null/undefined on error) and
capture the error value locally, then in a useEffect dependent on that error
value update setLastError and call console.warn (or process the error) so
logging and state updates run after render and avoid Strict Mode
double-invocation issues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 74dc10a4-765d-401c-85f4-985db12d156e

📥 Commits

Reviewing files that changed from the base of the PR and between 370252e and 832879f.

⛔ Files ignored due to path filters (2)
  • web/src/korrel8r/client/sdk.gen.ts is excluded by !**/*.gen.*
  • web/src/korrel8r/client/types.gen.ts is excluded by !**/*.gen.*
📒 Files selected for processing (15)
  • korrel8r/korrel8r-openapi.yaml
  • web/locales/en/plugin__troubleshooting-panel-console-plugin.json
  • web/src/__tests__/types.spec.ts
  • web/src/components/AIExperienceIcon.tsx
  • web/src/components/AgentMenu.tsx
  • web/src/components/Korrel8rPanel.tsx
  • web/src/hooks/useKorrel8r.ts
  • web/src/hooks/useLocationQuery.ts
  • web/src/hooks/usePopover.tsx
  • web/src/hooks/useTroubleshootingPanel.tsx
  • web/src/korrel8r-client.ts
  • web/src/korrel8r/client/index.ts
  • web/src/korrel8r/types.ts
  • web/src/redux-actions.ts
  • web/src/redux-reducers.ts
✅ Files skipped from review due to trivial changes (2)
  • web/src/components/AIExperienceIcon.tsx
  • web/src/korrel8r/client/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/src/hooks/usePopover.tsx
  • web/src/components/Korrel8rPanel.tsx

Comment on lines +193 to +194
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 2, maxDelay);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the backoff delay abortable.

Both retry loops wait on a bare setTimeout, so cancel() can stay stuck for up to maxDelay before the loop actually exits. That leaves reconnect timers alive after unmount/disable and can trigger late retries against stale state.

Possible fix
+const sleep = (ms: number, signal: AbortSignal) =>
+  new Promise<void>((resolve, reject) => {
+    const id = setTimeout(resolve, ms);
+    const onAbort = () => {
+      clearTimeout(id);
+      reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
+    };
+    signal.addEventListener('abort', onAbort, { once: true });
+  });
+
 export const consoleEvents = (
   onEvent: (event: Console) => void | Promise<void>,
   onError: (err: unknown) => void,
   onConnect: () => void,
@@
       } catch (err) {
         if (controller.signal.aborted) break;
         onError(err);
-        await new Promise((r) => setTimeout(r, delay));
+        await sleep(delay, controller.signal);
         delay = Math.min(delay * 2, maxDelay);
       }
     }
   };
@@
       } catch (err) {
         onError(err); // Set error even if canceled, we are still disconnected
         if (controller.signal.aborted) break;
-        await new Promise((r) => setTimeout(r, delay));
+        await sleep(delay, controller.signal);
         delay = Math.min(delay * 2, maxDelay);
       }
     }
   };

Also applies to: 219-220

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/korrel8r-client.ts` around lines 193 - 194, The exponential backoff
wait using await new Promise((r) => setTimeout(r, delay)) is not abortable and
lets cancel() block until the timeout elapses; change both retry loops to use an
abortable delay that races setTimeout against an AbortSignal (or rejects when
the provided signal is aborted) so the loop can exit immediately on
cancellation, update the cancel() path to call abort() on that signal (or
otherwise resolve the delay promise), and replace the two occurrences that
manipulate delay and maxDelay (the backoff logic around delay/maxDelay) with
calls to this abortableDelay helper so reconnect timers don't fire after
unmount/disable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants