Conversation
Add a lightweight API endpoint to fetch trace timeline data (id, startTime, endTime, status) grouped by session ID. This supports rendering timeline bars in the new session view without loading full trace data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build 6 presentational components for the new session table: header, session row with chevron/time/ID/totals/timeline, totals pill, traces timeline with proportional bars, trace section header, and trace card with placeholder input/output columns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace InfiniteDataTable with @tanstack/react-virtual virtualizer for sessions view. Implements flat list computation (session rows, trace section headers, trace cards), expand/collapse with trace fetching, timeline data fetching, IntersectionObserver infinite scroll, and trace card click navigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… fixes Add loading/empty states for session expansion, initial session loading, and empty results. Handle edge cases: null-safe cost/token rendering, timeline minimum bar widths, back-to-back trace gap filtering, sticky header, gradient fade color fix, and title tooltips for truncated IDs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, row click) Reset expanded/trace/timeline state when query params change to prevent stale data. Add stable getItemKey to virtualizer for smooth expand/collapse. Wire session row click to toggle expand matching cursor-pointer affordance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove /1000 division in formatDuration since SessionRow.duration is already in seconds - Change input/output columns from overflow-clip to overflow-y-auto for scrollability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…pand animations Fix gradient overlays to use absolute positioning without sticky conflict, add pointer-events-none so gradients don't block scrolling. Extend placeholder text to 3+ paragraphs to demonstrate scrolling within 140px cards. Update estimateSize to match the new 140px card height. Add framer-motion fade-in animations (opacity + scale spring) for trace cards, section headers, and loading/empty states on session expand. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… defaults Merge timeline fetching into the sessions endpoint to eliminate a second network call. Extract session expansion state into a dedicated Zustand store following existing codebase patterns. Change default time range from 24h to 72h and remove incorrect .toReversed() on already-descending trace results. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change gradient overlays from absolute to sticky positioning so they stay anchored at the bottom of the scrollable viewport instead of scrolling away with content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wrap virtualizer items with AnimatePresence and add exit props (opacity fade + slight scale) to motion.div wrappers so collapsing a session provides a smooth visual transition instead of instant disappearance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix keyboard navigation including collapsed session traces by filtering allVisibleTraceIds to only expanded sessions in rendered order - Add LIMIT 1000 to timeline query to prevent unbounded row fetches - Validate pastHours with parseFloat check and use UInt32 parameter type matching the pattern in query-builder.ts - Surface fetch errors in SessionsVirtualList with retry option Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Prevent re-expanding a session while its trace fetch is in-flight - Clear expanded state when refresh button is clicked - Increase timeline query LIMIT from 1000 to 5000 for multi-session pages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
setSessionTraces now checks if the session is still expanded before writing. Prevents stale in-flight responses from repopulating traces after resetExpandState clears state on filter/date changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace overlapping concurrency guards (loadingSessions check, expandedSessions stale-write guard) with AbortController pattern. Controllers live in closure scope inside createSessionsStore, keeping non-serializable objects out of Zustand state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…elines - Remove unused abortSession/abortAllSessions from store (abort logic is already inline in collapseSession/resetExpandState) - Add fetchVersionRef to discard stale timeline data when params change mid-flight Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Timeline bars are now clickable — opens trace side panel - Fix gradient overlay scrolling with content by wrapping text in scrollable inner div, keeping gradient absolute in outer container - Animate expanded items with height 0→auto instead of scale - Simplify onTraceClick to take traceId string instead of TraceRow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Timeline bars are clickable — opens trace side panel - Session ID and trace card ID click-to-copy via CopyTooltip - Fix gradient overlay scrolling (inner scroll wrapper) - Unified layout: header always mounted, states swap underneath - Height accordion animation on expand, no exit animation - Simplified onTraceClick to take traceId string Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lick race toggleSessionExpanded was symmetric — double-clicking before React re-rendered would undo the expand. Replaced with expandSession (add-only) and collapseSession (remove-only) for directional intent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Query ClickHouse for the main agent's first and last LLM spans (grouped by system prompt hash) to extract user input and assistant output. Render both columns with the shared Markdown component. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The first LLM span's last user message is the actual task input, not the first user message which may be a system-level prompt. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… ASC - Consolidate wrapper div, gradient, and expand button into TraceIOContent - Expand button toggles collapse/expand (shared state between input/output) - Chevron icon (ChevronDown/Up) replaces "Expand" text, shows on group-hover - Gradient always visible; chevron opacity controlled by group-hover - Sort trace cards within a session ascending by startTime - Fix extractLastUserMessage to use findLast, skipping injected system-reminders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sessions store Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ct fetch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…) for trace ID validation ClickHouse-generated trace IDs are valid 8-4-4-4-12 hex UUIDs but do not conform to RFC 4122 version/variant constraints, causing z.string().uuid() to reject every real trace ID with HTTP 400. Replace with a regex that accepts any well-formed hex UUID regardless of version or variant nibble. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… guard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… single source of truth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR replaces the sessions table with a virtualised, expandable list ( Key changes and findings:
Confidence Score: 3/5Merge-safe for UI/UX but the N+1 ClickHouse query pattern in the new batch endpoint may cause measurable latency degradation or connection-pool pressure in production under normal usage. Two concrete, verifiable bugs remain: the N+1 ClickHouse query pattern that fires up to 300 queries per batch request (P1 performance/reliability), and the memory-cache TTL being silently ignored for non-Redis deployments (P1 correctness). The N+1 issue in particular is on the hot path for the new sessions view and will be triggered by normal user interactions, making it a production reliability concern that warrants a fix before merge.
|
| Filename | Overview |
|---|---|
| frontend/lib/actions/sessions/trace-io.ts | New batched trace I/O fetch — fires up to 3 ClickHouse queries per trace in parallel (N+1 pattern), which at 100-trace batches can issue 300 simultaneous point-lookup queries. |
| frontend/lib/cache.ts | Adds expire helper (correct); however, the set method's non-Redis path ignores expireAfterSeconds, causing new regex cache entries to persist indefinitely in memory-only deployments. |
| frontend/lib/actions/sessions/extract-input.ts | New LLM-assisted regex extraction with caching; partial-match break discards previously resolved results when LLM regeneration subsequently fails. |
| frontend/lib/actions/sessions/utils.ts | Server-side sort implemented safely: sortDirection validated via z.enum and guarded with a ternary before interpolation — previously flagged SQL-injection vector is properly fixed. |
| frontend/components/traces/sessions-table/index.tsx | Substantial refactor replacing table with virtualized list; session expansion, sort, and pagination wiring look correct. Fetch error handling shows a toast as required by project conventions. |
| frontend/components/traces/sessions-table/sessions-store.tsx | New Zustand store managing expand/loading/trace state with closure-held AbortController map; JS single-threaded model makes the concurrent-access model safe for the current usage pattern. |
| frontend/components/traces/sessions-table/sessions-virtual-list.tsx | Virtualised list with sticky expanded session headers; range extraction and IO-loading logic look correct. Relies on useBatchedTraceIO debouncing to avoid excessive network requests. |
| frontend/components/traces/sessions-table/use-batched-trace-io.ts | Debounced batched fetch with LRU cache and toast on error; correctly handles abort and deduplication of in-flight/pending IDs. |
| frontend/lib/actions/sessions/parse-input.ts | Multi-provider message parsing (OpenAI/Anthropic/Gemini); Gemini extraction guard on line 128 has ambiguous logic for role-less content (pre-existing, flagged in prior thread). |
| app-server/src/routes/spans.rs | New POST /skeleton-hashes endpoint validates input size (1–200) and returns structural hashes; clean error handling with no panics. |
Comments Outside Diff (1)
-
frontend/lib/cache.ts, line 94-96 (link)expireAfterSecondssilently ignored in non-Redis environmentsWhen
REDIS_URLis not set, theelsebranch stores every entry withexpiresAt: null, completely ignoring theexpireAfterSecondsoption. The new LLM-generated regex cache inextract-input.tscalls:await cache.set(cacheKey, regex, { expireAfterSeconds: SEVEN_DAYS_SECONDS })
In a non-Redis deployment, this regex is cached forever — the intent of a 7-day rolling TTL is silently lost, and the in-memory Map grows without bound as more unique
systemHashvalues are seen.Apply the TTL in the memory path too:
} else { const expiresAt = options.expireAfterSeconds ? Date.now() + options.expireAfterSeconds * 1000 : options.expireAt ? options.expireAt.getTime() : null; this.memoryCache.set(key, { value, expiresAt }); }
Reviews (6): Last reviewed commit: "feat: fix comment" | Re-trigger Greptile
| try { | ||
| const cachedRegex = await cache.get<string>(cacheKey); | ||
| if (cachedRegex) { | ||
| let allMatched = true; | ||
| for (const trace of traces) { | ||
| const joinedText = joinUserParts(trace.parsed?.userParts ?? []); | ||
| if (!joinedText) { | ||
| results[trace.traceId] = { input: null, output: trace.output }; | ||
| continue; | ||
| } | ||
| const extracted = applyRe2Regex(cachedRegex, joinedText); | ||
| if (extracted) { | ||
| results[trace.traceId] = { input: extracted, output: trace.output }; | ||
| } else { | ||
| allMatched = false; | ||
| break; | ||
| } | ||
| } | ||
| if (allMatched) { | ||
| await cache.expire(cacheKey, SEVEN_DAYS_SECONDS).catch(() => {}); | ||
| return; | ||
| } | ||
| await cache.remove(cacheKey).catch(() => {}); |
There was a problem hiding this comment.
Concurrent requests with the same
systemHash will both call the LLM
When a cached regex exists but partially fails (allMatched = false), cache.remove is called and the function falls through to regenerate. Between cache.remove and the new cache.set, any concurrent request that shares the same systemHash will also find a cache miss and independently invoke generateExtractionRegex. Since generateExtractionRegex is non-deterministic (LLM), the two concurrent calls could produce slightly different regexes, and the last write wins.
Per the project's rule on global mutable state and race conditions, this should at least be documented. If different regex results are a real concern, a distributed lock or a "write-once" check on the cached value would be safer:
// Only cache if the key is still absent (write-once semantics)
const stillMissing = !(await cache.exists(cacheKey));
if (anyMatch && stillMissing) {
await cache.set(cacheKey, regex, { expireAfterSeconds: SEVEN_DAYS_SECONDS }).catch(() => {});
}Rule Used: Document any potential race conditions with global... (source)
Learnt From
lmnr-ai/lmnr-ts#72
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Exported
TraceTimelineItemtype is unused dead code- Removed the unused exported
TraceTimelineItemtype from the traces types module.
- Removed the unused exported
- ✅ Fixed: Stale closure causes session toggle to malfunction
- Added a store-level
isSessionExpandedgetter and used it in the toggle handler so expansion state is read from current store state instead of a stale closure.
- Added a store-level
- ✅ Fixed: Missing shallow equality causes excessive re-renders
- Updated the actions selector
useSessionsStoreContextcall to useshallow, preventing unnecessary re-renders from new object literals.
- Updated the actions selector
Or push these changes by commenting:
@cursor push 30ffabbb6e
Preview (30ffabbb6e)
diff --git a/frontend/components/traces/sessions-table/index.tsx b/frontend/components/traces/sessions-table/index.tsx
--- a/frontend/components/traces/sessions-table/index.tsx
+++ b/frontend/components/traces/sessions-table/index.tsx
@@ -63,6 +63,7 @@
const {
expandSession,
+ isSessionExpanded,
collapseSession,
setLoadingSession,
setSessionTraces,
@@ -70,16 +71,20 @@
setLoadingSessionIO,
resetExpandState,
getController,
- } = useSessionsStoreContext((state) => ({
- expandSession: state.expandSession,
- collapseSession: state.collapseSession,
- setLoadingSession: state.setLoadingSession,
- setSessionTraces: state.setSessionTraces,
- mergeTraceIO: state.mergeTraceIO,
- setLoadingSessionIO: state.setLoadingSessionIO,
- resetExpandState: state.resetExpandState,
- getController: state.getController,
- }));
+ } = useSessionsStoreContext(
+ (state) => ({
+ expandSession: state.expandSession,
+ isSessionExpanded: state.isSessionExpanded,
+ collapseSession: state.collapseSession,
+ setLoadingSession: state.setLoadingSession,
+ setSessionTraces: state.setSessionTraces,
+ mergeTraceIO: state.mergeTraceIO,
+ setLoadingSessionIO: state.setLoadingSessionIO,
+ resetExpandState: state.resetExpandState,
+ getController: state.getController,
+ }),
+ shallow
+ );
// Serialize filter array for stable dependency comparison
const filterKey = JSON.stringify(filter);
@@ -157,7 +162,7 @@
const handleToggleSession = useCallback(
async (sessionId: string) => {
- const isExpanded = expandedSessions.has(sessionId);
+ const isExpanded = isSessionExpanded(sessionId);
if (isExpanded) {
collapseSession(sessionId);
@@ -226,13 +231,13 @@
}
},
[
- expandedSessions,
pastHours,
startDate,
endDate,
projectId,
toast,
expandSession,
+ isSessionExpanded,
setLoadingSession,
setSessionTraces,
mergeTraceIO,
diff --git a/frontend/components/traces/sessions-table/sessions-store.tsx b/frontend/components/traces/sessions-table/sessions-store.tsx
--- a/frontend/components/traces/sessions-table/sessions-store.tsx
+++ b/frontend/components/traces/sessions-table/sessions-store.tsx
@@ -15,6 +15,7 @@
};
export type SessionsActions = {
+ isSessionExpanded: (sessionId: string) => boolean;
expandSession: (sessionId: string) => void;
collapseSession: (sessionId: string) => void;
setLoadingSession: (sessionId: string, loading: boolean) => void;
@@ -40,9 +41,11 @@
export const createSessionsStore = () => {
const sessionControllers = new Map<string, AbortController>();
- return createStore<SessionsStore>()((set) => ({
+ return createStore<SessionsStore>()((set, get) => ({
...DEFAULT_STATE,
+ isSessionExpanded: (sessionId) => get().expandedSessions.has(sessionId),
+
expandSession: (sessionId) =>
set((state) => {
const next = new Set(state.expandedSessions);
diff --git a/frontend/lib/traces/types.ts b/frontend/lib/traces/types.ts
--- a/frontend/lib/traces/types.ts
+++ b/frontend/lib/traces/types.ts
@@ -191,14 +191,6 @@
outputMessageIds: string[];
};
-export type TraceTimelineItem = {
- id: string;
- name?: string;
- startTime: string;
- endTime: string;
- status: string;
-};
-
// We have id and sessionId here because
// its not possible to make good type intersection,
// and use it in tanstack table wrappers.This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
|
Tip: Greploop — Automatically fix all review issues by running Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal. |
frontend/components/traces/sessions-table/session-table-header/sortable-header-cell.tsx
Show resolved
Hide resolved
frontend/components/traces/sessions-table/session-time-range.tsx
Outdated
Show resolved
Hide resolved
| const content = contents[i]; | ||
| if (content.role !== "user" && (content.role || i === 0)) continue; | ||
|
|
There was a problem hiding this comment.
Ambiguous
continue condition may process wrong message
The compound guard on the skip condition is confusing and can produce incorrect results:
if (content.role !== "user" && (content.role || i === 0)) continue;When iterating backwards, for any message at index i > 0 that has no role set (null / undefined), the expression evaluates:
content.role !== "user"→true(content.role || i === 0)→(falsy || false)→false
So the guard does not skip it — the function returns that roleless message as if it were the last user turn. A Gemini streaming response whose final content object lacks a role field would be incorrectly treated as user input.
If the intent is to fall back to the last rolelessly-typed message, this should be documented explicitly. Otherwise, simplify to:
| const content = contents[i]; | |
| if (content.role !== "user" && (content.role || i === 0)) continue; | |
| if (content.role !== "user") continue; |
frontend/components/traces/sessions-table/sessions-virtual-list.tsx
Outdated
Show resolved
Hide resolved
| const cachedRegex = await cache.get<string>(cacheKey); | ||
| if (cachedRegex) { | ||
| let allMatched = true; | ||
| for (const trace of traces) { | ||
| const joinedText = joinUserParts(trace.parsed?.userParts ?? []); | ||
| if (!joinedText) { | ||
| results[trace.traceId] = { input: null, output: trace.output }; | ||
| continue; | ||
| } | ||
| const extracted = applyRe2Regex(cachedRegex, joinedText); | ||
| if (extracted) { | ||
| results[trace.traceId] = { input: extracted, output: trace.output }; | ||
| } else { | ||
| allMatched = false; | ||
| break; | ||
| } | ||
| } | ||
| if (allMatched) { | ||
| await cache.expire(cacheKey, SEVEN_DAYS_SECONDS).catch(() => {}); | ||
| return; | ||
| } | ||
| await cache.remove(cacheKey).catch(() => {}); |
There was a problem hiding this comment.
Partial cached-regex results are silently discarded when LLM regeneration fails
When the cached regex matches some traces but fails on one (causing allMatched = false + break), the results for the already-matched traces are written into results. The function then falls through to the LLM path.
If generateExtractionRegex returns null (LLM timeout, API unavailable), the code does:
if (!regex) {
for (const trace of traces) {
results[trace.traceId] = { input: joinUserParts(...), output: trace.output };
}
return;
}This overwrites the previously-extracted (and more refined) values for the traces that did match the cached regex with raw unfiltered text. The user experiences degraded output quality for no reason.
One simple fix: preserve the results that were successfully extracted before the fallback overwrites them:
if (!regex) {
for (const trace of traces) {
if (!(trace.traceId in results)) {
results[trace.traceId] = { input: joinUserParts(trace.parsed?.userParts ?? []), output: trace.output };
}
}
return;
}There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 16af4cf. Configure here.


Note
Medium Risk
Medium risk due to a substantial rewrite of the sessions UI plus new backend endpoints and LLM-assisted input extraction/caching for trace previews, which could impact performance and query correctness.
Overview
Sessions view is rebuilt from the old
InfiniteDataTable/column-based UI to a custom virtualized list with expandable sessions, sticky headers, and a newSessionsStoreto manage expanded state, loading, and per-session trace results (with request aborting).Adds server-driven sorting and improved filtering for sessions via new
sortColumn/sortDirURL params, updatedGetSessionsSchema, and updated ClickHouse query builder ordering; advanced search now skips incomplete/empty filter tags and can omitresourceto disable autocomplete.Introduces batched trace IO previews: a new Next.js API route
POST /api/projects/[projectId]/traces/ioreturns per-trace{input, output}previews, with client-side debounced/LRU-cached fetching for visible trace cards.Backend support for preview grouping/caching: adds
POST /api/v1/projects/{project_id}/skeleton-hashesin Rust (1–200 texts) to compute structural hashes used to group traces by system prompt, plus new frontend extraction logic that uses an LLM to generate RE2-compatible regexes and caches them (with new cacheexpiresupport) to extract the “true” user input from scaffolded messages.Reviewed by Cursor Bugbot for commit 16af4cf. Bugbot is set up for automated code reviews on this repo. Configure here.