Skip to content

feat: Laminar Agent LAM-1335#1448

Open
kolbeyang wants to merge 45 commits intodevfrom
feat/LAM-1335
Open

feat: Laminar Agent LAM-1335#1448
kolbeyang wants to merge 45 commits intodevfrom
feat/LAM-1335

Conversation

@kolbeyang
Copy link
Copy Markdown
Collaborator

@kolbeyang kolbeyang commented Mar 19, 2026

Note

Medium Risk
Adds a new AI agent API that can execute arbitrary SELECT SQL and fetch trace context, and rewires core trace/signal UX to integrate it; this touches data-access paths and navigation behavior. Also removes the existing per-trace chat persistence endpoints, so regressions in trace-assistant workflows are possible.

Overview
Introduces a new Laminar Agent experience: a project-wide chat panel (collapsed button, floating drawer, or side-by-side resizable panel) that persists messages across remounts and can deep-link to spans via clickable <span .../> references.

Adds POST /api/projects/[projectId]/agent which streams model responses using a new getGlobalAgentSystemPrompt and exposes two tools: compactTraceContext (trace structure via getTraceStructureAsString) and executeSql (runs SELECT queries with 100-row truncation). The previous trace-scoped chat API routes and server-side message storage/streaming logic are removed.

Signals/trace UI is updated to integrate the agent: signal event drawers now load a single event via a new GET /signals/[id]/events/[eventId], event selection is driven by the eventId query param (with table auto-scroll to the focused row), and trace view gets a new signals dropdown that can prefill the agent to explain a specific signal on a trace.

Written by Cursor Bugbot for commit b3db65d. This will update automatically on new commits. Configure here.

kolbeyang and others added 30 commits March 18, 2026 13:40
…nuqs

- Create LaminarAgentStore with viewMode, prefillInput, chatMessages, chatStatus state
- Create useSpanId() nuqs hook for URL-driven span selection
- Remove selectedSpan usage from main trace view, derive from spans + spanId URL param
- Update condensed-timeline to use useSpanId() hook
- Update mini-tree to use useSpanId() instead of manual URL manipulation
- Update shared trace view to use useSpanId() hook
- Keep selectedSpan/setSelectedSpan in base store for debugger session backward compat
- Add getSpanById() helper to base store

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-fetching

- Create useEventId() nuqs hook for URL-driven event selection
- Update signals page to use useEventId() instead of selectedEvent from store
- Update events table to set eventId via nuqs on row click
- Make EventDetailPanel accept eventId instead of event prop, fetch its own data
- Add GET /api/projects/{projectId}/signals/{signalId}/events/{eventId} endpoint
- Add loading skeleton and error states to EventDetailPanel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…o-scroll

- Replace state.selectedSpan from Zustand store with useSpanId() hook
  in span-card.tsx, list-item.tsx, tree/index.tsx, and list/index.tsx
- The nuqs migration stopped calling setSelectedSpan(), leaving it
  always undefined, which broke highlight and auto-scroll
- isSelected now compares against the spanId URL param
- selectedSpanIndex now derives from the spanId URL param

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create collapsed FAB button (bottom-right) with Laminar icon
- Create floating panel (400px wide, 70vh tall, fixed right side)
- Create side-by-side wrapper using ResizablePanelGroup
- Create shared AgentPanel with header, mode-switch buttons, tooltips
- Create root LaminarAgent component orchestrating view modes
- Mount collapsed button, floating panel, and side-by-side wrapper in project layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use transition-all on collapsed FAB to animate both shadow and transform
- Add bg-background to floating panel outer div for sub-pixel gap prevention
- Add withHandle prop to ResizableHandle for visible drag affordance
- Align floating panel to bottom-6 right-6 matching FAB position
- Bump floating panel z-index to z-[60] to stay above sheets/dialogs
- Confirm icon logic correctness (INFO item, no code change needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create global agent API route at /api/projects/[projectId]/agent
- Add two tools: compactTraceContext (trace analysis) and executeSql (SQL queries)
- Create system prompt with table schemas, enum values, and tool usage guidelines
- Implement full chat UI in agent-panel.tsx using useChat + DefaultChatTransport
- Add tool call rendering cards (CompactTraceCard, SqlToolCard with expandable SQL)
- Implement inline span buttons in markdown responses for trace navigation
- Support prefill input from Zustand store for suggestions integration
- Reuse existing Conversation, Response, and CodeHighlighter components

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show streaming indicator for both submitted and streaming status
- Disable send button during submitted status to prevent double-send
- Clear prefill ref after consumption so same text can prefill again
- Add cursor-pointer, hover, and focus-visible styles to inline span buttons
- Disable New Chat button during streaming/submitted
- Add gradient overlay at top of messages area matching chat.tsx
- Add minimal-scrollbar class to scroll container
- Use variant="default" instead of variant="ghost" + bg-primary on send button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All items resolved in 01797e6 (code fixes) or marked as
intentional (empty state icon, CompactTraceCard asymmetry).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ase 4)

- Create suggestions.ts with route-pattern-to-suggestion mapping
- Update collapsed-button.tsx with animated suggestion cycling (5s interval)
- Clicking a suggestion prefills the agent prompt and opens floating mode
- Create signals-pill.tsx that queries signal_events for the current trace
- Signals pill shows dropdown with submenu: explain via agent or open in signals
- Replace "Chat with trace" pill in header with SignalsPill component
- Remove chatOpen/setChatOpen props from Header and trace-view/index.tsx
- Remove Chat component import and usage from trace-view/index.tsx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ments

- Replace server-only executeQuery with client-side fetch to /api/projects/.../sql route
- Add UUID validation for traceId to prevent SQL injection
- Combine multiple useLaminarAgentStore selectors with shallow equality (signals-pill, collapsed-button)
- Fix suggestion exit animation to slide left (x: -10) for natural flow
- Pause suggestion cycling on hover with mouseenter/mouseleave handlers
- Add "Signals" text label to loading state spinner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All Phase 4 QA/Designer issues resolved in fc59004.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…an navigation

- Add traceIdContext to agent store for cross-page span navigation
- Agent span buttons navigate to trace page when not already on it
- Sync traceIdContext when navigating to trace pages
- Add auto-scroll to focused row in InfiniteDataTable for eventId deep links
- Reset scroll tracking when focusedRowId changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…llow

- Combine six individual useLaminarAgentStore calls into one with shallow equality
- Follows CLAUDE.md best practice to avoid unnecessary rerenders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show EventDetailPanel when eventId is present regardless of traceId
  in signal/index.tsx (deep link from "Open in Signals" now works)
- Merge two useEffects for auto-scroll in infinite-datatable so reset
  and scroll happen in a single effect (fixes subsequent scroll failures)
- Disable span buttons visually with tooltip when no trace context is
  available in agent-panel.tsx
- Replace fragile pushState + PopStateEvent with router.replace for
  nuqs-compatible span navigation on trace pages
- Clear traceIdContext when navigating away from trace pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 5 open items resolved in b7f6f24:
- EventDetailPanel deep link condition
- Auto-scroll useEffect merge
- Span button disabled state with tooltip
- router.replace instead of pushState
- traceIdContext cleanup on navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Only include eventId param so EventDetailPanel renders without
  TraceView covering it
- Users can still open the trace via the "View trace" button inside
  EventDetailPanel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…yboard shortcuts

- Tune system prompt for more reliable tool usage: explicit rules for
  when to use compactTraceContext vs executeSql, signal event handling
- Add isAiProviderConfigured() check; hide FAB when no AI provider set
- Add keyboard shortcuts: Cmd+Shift+L to toggle, Escape to collapse
- Make floating panel responsive with max-w/max-h constraints
- Delete old chat.tsx (all functionality migrated to global agent)
- Remove dead API routes: trace-scoped agent/messages, new-chat, route
- Remove dead server actions: trace/agent/stream, messages, prompt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ortal

- Use a single AgentPanel instance mounted in LaminarAgent
- Portal AgentPanel into SideBySideWrapper container for side-by-side mode
- SideBySideWrapper provides a container ref via Zustand store
- Delete floating-panel.tsx (inlined into index.tsx)
- Messages now persist when switching between floating and side-by-side

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sync useChat messages to/from store on mount/unmount to preserve chat
  across mode switches (portal remount)
- Fix Cmd+Shift+L shortcut by using e.key.toLowerCase() instead of
  exact match (Shift produces uppercase "L")
- Lower floating panel z-index from z-[60] to z-40 so dialogs (z-50)
  render above it
- Skip Escape-to-collapse when focus is in textarea/input/contenteditable
- Combine 4 separate useLaminarAgentStore calls into single selector
  with shallow equality
- Always render ResizablePanelGroup in SideBySideWrapper and control
  agent panel via resize(0)/resize(35) to prevent children remount
- Add aiEnabled field to store (set from server layout via initializer
  component) and conditionally hide "Explain signal" option in
  SignalsPill when AI is not configured
- Add min-h-[300px] to floating panel for short viewports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tions matching

- Fix side-by-side panel not expanding: call panel.expand() before
  panel.resize(35) and panel.collapse() instead of panel.resize(0),
  since resize() fails silently on a collapsed panel with defaultSize={0}
- Fix trace-specific suggestions never matching: update getSuggestionsForRoute
  to accept search params and match trace detail when traceId param exists,
  since actual trace URLs use ?traceId= not /traces/{traceId}
- Fix floating panel click-through: add pointer-events-auto and
  stopPropagation on the floating panel container
- Fix suggestion text clipping: add max-w-[200px] truncate on suggestion text
- Add aria-label attributes to mode-switch and close buttons in agent panel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All items resolved in dbef233 or marked out of scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ble-panels v4

In react-resizable-panels v4, bare numbers passed to panel.resize()
are interpreted as pixels, not percentages. This caused the side-by-side
panel to expand to only ~35px instead of 35% width.

Fix: use "35%" string for resize() and percentage strings for
defaultSize/minSize props. Also remove collapsible/collapsedSize
props which are unnecessary when controlling size programmatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…de-switch message persistence

- Extract traceId from search params via useSearchParams() as fallback
  when URL uses ?traceId= query param instead of path segment
- Update isOnTracePage to match /project/*/traces with traceId in search params
- Raise floating panel and FAB from z-40/z-50 to z-[55] so they appear
  above trace detail panel (z-50)
- Raise dialog overlay and content from z-50 to z-[60] so dialogs still
  appear above the floating agent panel
- Fix conversation history loss during mode switch by syncing messages
  to store on every update (not just unmount) and reading initial
  messages from a ref snapshot to avoid React batching timing issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…and floating panel position

- Add `relative` and `overflow-hidden` to left panel content div so
  absolutely-positioned children (trace detail, event detail) are
  contained within the left panel bounds in side-by-side mode
- Change floating panel from `bottom-6` to `top-6` so it anchors to
  the top-right of the viewport instead of bottom-right
- In non-side-by-side mode, the left panel is 100% width so behavior
  is unchanged; in side-by-side mode, absolute elements now correctly
  position relative to the left panel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kolbeyang and others added 13 commits March 18, 2026 19:39
…oper clipping

The ResizablePanel's internal flex-grow div was expanding beyond the
panel bounds, causing content to overflow. Adding overflow-hidden to
the panel ensures children are visually clipped at the panel boundary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change FAB button icon from Laminar logo to Sparkles (lucide-react)
- Floating window now extends full viewport height (top to bottom with gap)
- Add left-edge draggable resize handle on floating window (min 320px, max 700px)
- Update view mode icons: Columns2 for floating header, Layers for side-by-side
- Make open-chat suggestions URL-dependent using shared route-to-suggestion map
- Each route now has >3 suggestions; "Summarize trace" only appears on trace pages
- Fix layout height: use flex-1 min-h-0 on ResizablePanelGroup for proper flex fill
- Remove withHandle from side-by-side ResizableHandle (cleaner appearance)
- Fix unnecessary SQL on trace summarization via explicit prompt instructions
- Move SQL copy button to card header, show only when expanded
- Fix span pill hover: use ring-2 instead of bg change to avoid size growth
- Update signals prompt to include payload request
- Change CompactTraceCard icon from Search to ListTree

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…vals page

- Add `flex flex-col` to left panel content wrapper in side-by-side-wrapper.tsx
  so flex children (Tabs, evaluations content) properly fill available height
  instead of overflowing the viewport. This single fix resolves both the trace
  table extending below the viewport and the evals page not filling height.
- Change floating panel gap from top-3/bottom-3 to top-4/bottom-4 for better
  consistency with FAB's 24px offset positioning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Switch Layers to Layers2 icon for floating mode toggle
- Thin resize handle (w-0.5) and raise z-index (z-50) for better UX
- Move scroll-to-bottom button to left-3 to avoid agent panel overlap
- Add .agents/, PLAN.md, SPEC.md, TESTING_PLAN.md, TODO.md to .gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eanup

- Consolidate CollapsedButton with keyed inner component for clean state reset
- Fix collapsed width not animating back (width: undefined → width: 40)
- Add AnimatePresence slide-in/out animation for floating panel
- Remove all keyboard shortcuts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…n stable

Extract SuggestionSpan as keyed child so route changes only remount the
suggestion text, not the sparkle button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tand fix

- Move New Chat button to header (reload, change view, close icons)
- Split tool-call-cards into one component per file
- Fix deprecated Zustand useStore signature with useShallow
- Remove resolved TODO/QUESTION comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: global store

* feat: add global store
…vents/URL params

- Signal events table: replace CustomEvent("open-trace") with direct store access
  via laminarAgentStore refs; clicking trace ID now opens trace panel, activates
  side-by-side mode, and prefills agent with signal analysis prompt
- Agent panel: replace URL-based span navigation (router.replace) with
  traceViewStore.selectSpanById() via refs, fixing span clicks in embedded
  trace views (signals page)
- Remove open-trace event listener from events-table/index.tsx
- Update canNavigateToSpan to use refs.traceView instead of URL-based detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-based selectedSpan

- Remove useSpanId() nuqs hook usage from all trace view components
- Restore selectedSpan and setSelectedSpan from Zustand store as source of truth
- Remove @deprecated JSDoc comments from store fields
- Update handleSpanSelect to use store + URL params for bookmarkability
- Revert tree, list, condensed-timeline, and shared trace view components
- Keep URL param updates for bookmarkability while store drives selection
- Fixes embedded trace view (signals page) where URL changes caused navigation

Files changed:
- store/base.ts: remove @deprecated from selectedSpan/setSelectedSpan
- index.tsx: use store selectedSpan, update URL via router.replace
- tree/span-card.tsx: read selectedSpan from store
- tree/index.tsx: read selectedSpan from store for scroll
- list/list-item.tsx: read selectedSpan from store
- list/index.tsx: read selectedSpan from store for scroll
- list/mini-tree.tsx: use selectSpanById + router.push
- condensed-timeline/index.tsx: use store selectedSpan/setSelectedSpan
- shared/traces/trace-view.tsx: use store selectedSpan/setSelectedSpan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s query

signal_events table does not have a project_id column, causing 500 errors
when fetching event details.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kolbeyang kolbeyang requested a review from olzhik11 March 19, 2026 18:07
@kolbeyang kolbeyang self-assigned this Mar 19, 2026
kolbeyang and others added 2 commits March 19, 2026 14:10
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kolbeyang kolbeyang marked this pull request as ready for review March 20, 2026 13:37
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: SQL injection via string interpolation of URL params
    • I added UUID validation for signalId and eventId in the route and return HTTP 400 for invalid identifiers before building the SQL query.
  • ✅ Fixed: Trace ID click unconditionally opens agent, breaks non-AI flow
    • I restored trace URL synchronization on trace-ID click and gated Laminar Agent opening/prefill logic behind the LAMINAR_AGENT feature flag.
  • ✅ Fixed: Event panel renders behind trace view simultaneously
    • I restored the !traceId condition for rendering the event drawer so event and trace panels are mutually exclusive.

Create PR

Or push these changes by commenting:

@cursor push 0be0ab07fe
Preview (0be0ab07fe)
diff --git a/frontend/app/api/projects/[projectId]/signals/[id]/events/[eventId]/route.ts b/frontend/app/api/projects/[projectId]/signals/[id]/events/[eventId]/route.ts
--- a/frontend/app/api/projects/[projectId]/signals/[id]/events/[eventId]/route.ts
+++ b/frontend/app/api/projects/[projectId]/signals/[id]/events/[eventId]/route.ts
@@ -1,14 +1,25 @@
 import { type NextRequest } from "next/server";
+import { z } from "zod/v4";
 
 import { executeQuery } from "@/lib/actions/sql";
 import { type EventRow } from "@/lib/events/types";
 
+const RouteParamsSchema = z.object({
+  signalId: z.string().uuid(),
+  eventId: z.string().uuid(),
+});
+
 export async function GET(
   _req: NextRequest,
   props: { params: Promise<{ projectId: string; id: string; eventId: string }> }
 ): Promise<Response> {
   const { projectId, id: signalId, eventId } = await props.params;
+  const parsedParams = RouteParamsSchema.safeParse({ signalId, eventId });
 
+  if (!parsedParams.success) {
+    return Response.json({ error: "Invalid signal or event identifier." }, { status: 400 });
+  }
+
   try {
     const query = `
       SELECT id, signal_id as signalId, trace_id as traceId, payload, timestamp

diff --git a/frontend/components/signal/events-table/columns.tsx b/frontend/components/signal/events-table/columns.tsx
--- a/frontend/components/signal/events-table/columns.tsx
+++ b/frontend/components/signal/events-table/columns.tsx
@@ -140,52 +140,62 @@
   },
 ];
 
-const staticColumnsAfterPayload: ColumnDef<EventRow>[] = [
-  {
-    accessorKey: "id",
-    cell: (row) => <Mono>{String(row.getValue())}</Mono>,
-    header: "ID",
-    size: 100,
-    id: "id",
-  },
-  {
-    accessorKey: "traceId",
-    header: "Trace ID",
-    cell: (row) => {
-      const traceId = String(row.getValue());
-      const event = row.row.original;
-      return (
-        <div className="flex items-center gap-1 min-w-0">
-          <button
-            className="font-mono text-xs min-w-0 truncate"
-            onClick={(e: MouseEvent) => {
-              e.stopPropagation();
-              // Open trace panel via signal store
-              const agentState = laminarAgentStore.getState();
-              const signalStore = agentState.refs.signal;
-              if (signalStore) {
-                signalStore.getState().setTraceId(traceId);
-              }
-              // Open Laminar Agent in side-by-side view with prefilled prompt
-              agentState.setViewMode("floating");
-              agentState.setPrefillInput(
-                `Show me the payload of this signal event ${event.id} formatted in a table, explain why it was detected on this trace ${traceId}, and detail which spans are relevant and why`
-              );
-            }}
-          >
-            {traceId}
-          </button>
-          <CopyTooltip value={traceId} className="">
-            <Copy className="size-3" />
-          </CopyTooltip>
-        </div>
-      );
+type BuildEventsColumnsOptions = {
+  onTraceIdClick?: (traceId: string) => void;
+  aiEnabled?: boolean;
+};
+
+function getStaticColumnsAfterPayload({
+  onTraceIdClick,
+  aiEnabled = false,
+}: BuildEventsColumnsOptions): ColumnDef<EventRow>[] {
+  return [
+    {
+      accessorKey: "id",
+      cell: (row) => <Mono>{String(row.getValue())}</Mono>,
+      header: "ID",
+      size: 100,
+      id: "id",
     },
-    size: 180,
-    id: "traceId",
-  },
-];
+    {
+      accessorKey: "traceId",
+      header: "Trace ID",
+      cell: (row) => {
+        const traceId = String(row.getValue());
+        const event = row.row.original;
+        return (
+          <div className="flex items-center gap-1 min-w-0">
+            <button
+              className="font-mono text-xs min-w-0 truncate"
+              onClick={(e: MouseEvent) => {
+                e.stopPropagation();
+                onTraceIdClick?.(traceId);
 
+                if (!aiEnabled) {
+                  return;
+                }
+
+                const agentState = laminarAgentStore.getState();
+                agentState.setViewMode("floating");
+                agentState.setPrefillInput(
+                  `Show me the payload of this signal event ${event.id} formatted in a table, explain why it was detected on this trace ${traceId}, and detail which spans are relevant and why`
+                );
+              }}
+            >
+              {traceId}
+            </button>
+            <CopyTooltip value={traceId} className="">
+              <Copy className="size-3" />
+            </CopyTooltip>
+          </div>
+        );
+      },
+      size: 180,
+      id: "traceId",
+    },
+  ];
+}
+
 const staticFilters: ColumnFilter[] = [
   {
     name: "ID",
@@ -204,7 +214,10 @@
   },
 ];
 
-export function buildEventsColumns(schemaFields: SchemaField[]): {
+export function buildEventsColumns(
+  schemaFields: SchemaField[],
+  options: BuildEventsColumnsOptions = {}
+): {
   columns: ColumnDef<EventRow>[];
   columnOrder: string[];
   filters: ColumnFilter[];
@@ -213,7 +226,7 @@
   const payloadColumns = validFields.map(createPayloadColumnDef);
   const payloadFilters = validFields.map(createPayloadFilter);
 
-  const columns = [...staticColumnsBeforePayload, ...payloadColumns, ...staticColumnsAfterPayload];
+  const columns = [...staticColumnsBeforePayload, ...payloadColumns, ...getStaticColumnsAfterPayload(options)];
 
   const columnOrder = ["timestamp", "traceId", ...validFields.map((f) => `payload:${f.name}`), "id"];
 

diff --git a/frontend/components/signal/events-table/index.tsx b/frontend/components/signal/events-table/index.tsx
--- a/frontend/components/signal/events-table/index.tsx
+++ b/frontend/components/signal/events-table/index.tsx
@@ -20,8 +20,10 @@
 import ColumnsMenu from "@/components/ui/infinite-datatable/ui/columns-menu";
 import DataTableFilter, { DataTableFilterList } from "@/components/ui/infinite-datatable/ui/datatable-filter";
 import { TableCell, TableRow } from "@/components/ui/table.tsx";
+import { useFeatureFlags } from "@/contexts/feature-flags-context";
 import { UNCLUSTERED_ID } from "@/lib/actions/clusters";
 import { type EventRow } from "@/lib/events/types";
+import { Feature } from "@/lib/features/features";
 import { useToast } from "@/lib/hooks/use-toast";
 
 import { buildEventsColumns } from "./columns";
@@ -61,8 +63,11 @@
   const [clusterId] = useClusterId();
   const [eventId, setEventId] = useEventId();
   const signal = useSignalStoreContext((state) => state.signal);
+  const setTraceId = useSignalStoreContext((state) => state.setTraceId);
   const selectedClusterIds = useSignalStoreContext((state) => getFilterClusterIds(state, clusterId), shallow);
   const isUnclusteredFilter = clusterId === UNCLUSTERED_ID;
+  const featureFlags = useFeatureFlags();
+  const aiEnabled = featureFlags[Feature.LAMINAR_AGENT];
   const searchParams = useSearchParams();
   const pathName = usePathname();
   const router = useRouter();
@@ -73,8 +78,21 @@
   const filterRaw = searchParams.getAll("filter");
   const filter = useMemo(() => filterRaw, [JSON.stringify(filterRaw)]);
 
-  const { columns, filters } = useMemo(() => buildEventsColumns(signal.schemaFields), [signal.schemaFields]);
+  const handleTraceIdClick = useCallback(
+    (traceId: string) => {
+      setTraceId(traceId);
+      const params = new URLSearchParams(searchParams.toString());
+      params.set("traceId", traceId);
+      router.push(`${pathName}?${params.toString()}`);
+    },
+    [pathName, router, searchParams, setTraceId]
+  );
 
+  const { columns, filters } = useMemo(
+    () => buildEventsColumns(signal.schemaFields, { onTraceIdClick: handleTraceIdClick, aiEnabled }),
+    [signal.schemaFields, handleTraceIdClick, aiEnabled]
+  );
+
   const fetchEvents = useCallback(
     async (pageNumber: number) => {
       try {

diff --git a/frontend/components/signal/index.tsx b/frontend/components/signal/index.tsx
--- a/frontend/components/signal/index.tsx
+++ b/frontend/components/signal/index.tsx
@@ -133,7 +133,7 @@
       )}
 
       <AnimatePresence>
-        {eventId && (
+        {eventId && !traceId && (
           <motion.div
             key="event-drawer"
             initial={{ x: 400 }}

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

WHERE signal_id = '${signalId}'
AND id = '${eventId}'
LIMIT 1
`;
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.

SQL injection via string interpolation of URL params

High Severity

The signalId and eventId URL path parameters are directly interpolated into a raw SQL query string without any sanitization or parameterization. While these values come from URL path segments, a crafted request could include SQL injection payloads (e.g., values containing single quotes) that modify the query logic. The existing executeQuery function sends this string directly to the backend ClickHouse endpoint.

Fix in Cursor Fix in Web

agentState.setViewMode("floating");
agentState.setPrefillInput(
`Show me the payload of this signal event ${event.id} formatted in a table, explain why it was detected on this trace ${traceId}, and detail which spans are relevant and why`
);
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.

Trace ID click unconditionally opens agent, breaks non-AI flow

Medium Severity

Clicking a trace ID in the events table unconditionally calls agentState.setViewMode("floating") and setPrefillInput(...), regardless of whether the Laminar Agent feature is enabled. When the feature flag is off, the LaminarAgent component isn't rendered, so the user sees no visible response to the agent-related actions. Additionally, the old code updated the URL with the traceId param via router.push(...), but this URL synchronization was removed, so the trace view state is no longer bookmarkable or back-button compatible.

Fix in Cursor Fix in Web


<AnimatePresence>
{selectedEvent && !traceId && (
{eventId && (
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.

Event panel renders behind trace view simultaneously

Low Severity

The !traceId guard was removed from the event detail panel visibility condition. Previously {selectedEvent && !traceId && ( ensured the event panel hid when a trace view was open. Now {eventId && ( allows both panels to render simultaneously — the event panel at z-40 rendered behind the trace view at z-50. This wastes resources and can cause visual glitches during animations.

Fix in Cursor Fix in Web

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants