From ca3ae3643dc7ba9b47a0e1129ff88f54bead5d95 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Thu, 2 Apr 2026 08:51:31 -0400 Subject: [PATCH] fix(site): session threads feedback (#23945) --- .../SessionThreadsPage/SessionThreadsPage.tsx | 15 ++- .../SessionThreadsPageView.tsx | 5 +- .../AgenticLoopTable.stories.tsx | 8 -- .../SessionTimeline/AgenticLoopTable.tsx | 9 -- .../SessionTimeline/PromptTable.tsx | 2 +- .../SessionTimeline/SessionTimeline.tsx | 127 ++++++++++-------- .../SessionTimelineSkeleton.stories.tsx | 12 ++ .../SessionTimelineSkeleton.tsx | 84 ++++++++++++ .../SessionTimeline/ToolCallTable.tsx | 2 +- 9 files changed, 186 insertions(+), 78 deletions(-) create mode 100644 site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimelineSkeleton.stories.tsx create mode 100644 site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimelineSkeleton.tsx diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPage.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPage.tsx index 5fe41793a7..69a569614b 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPage.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPage.tsx @@ -1,6 +1,6 @@ import type { FC } from "react"; import { useInfiniteQuery } from "react-query"; -import { useNavigate, useParams } from "react-router"; +import { useLocation, useNavigate, useParams } from "react-router"; import { infiniteSessionThreads } from "#/api/queries/aiBridge"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { useDashboard } from "#/modules/dashboard/useDashboard"; @@ -13,6 +13,7 @@ const SessionThreadsPage: FC = () => { const { permissions } = useAuthenticated(); const { entitlements } = useDashboard(); const navigate = useNavigate(); + const location = useLocation(); const { isEntitled, isEnabled, hasPermission } = getAIBridgePermissions( entitlements, @@ -45,7 +46,17 @@ const SessionThreadsPage: FC = () => { onFetchNextPage={sessionQuery.fetchNextPage} isAISessionsEnabled={isEnabled} isAISessionsEntitled={isEntitled} - onBackClicked={() => navigate(-1)} + onBackClicked={() => { + // location.key is "default" when the user navigated directly to + // this page (e.g. by refreshing or opening in a new tab). if there + // is a previous page in the history stack, navigate back. otherwise, + // navigate to the sessions list page without params + if (location.key === "default") { + navigate("/aibridge/sessions"); + } else { + navigate(-1); + } + }} /> ); diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx index cf017fb9cf..0d9e53ae5c 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionThreadsPageView.tsx @@ -16,6 +16,7 @@ import { import { AIBridgeSetupAlert } from "../AIBridgeSetupAlert"; import { SessionSummaryTable } from "./SessionSummaryTable"; import { SessionTimeline } from "./SessionTimeline/SessionTimeline"; +import { SessionTimelineSkeleton } from "./SessionTimeline/SessionTimelineSkeleton"; const SessionSummaryTooltip: FC = ({ children }) => ( @@ -118,7 +119,7 @@ export const SessionThreadsPageView: FC = ({ )}
- {session && ( + {session ? ( = ({ isFetchingNextPage={isFetchingNextPage} onFetchNextPage={onFetchNextPage} /> + ) : ( + loading && )}
diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/AgenticLoopTable.stories.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/AgenticLoopTable.stories.tsx index 3826d356b6..8b234e3af6 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/AgenticLoopTable.stories.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/AgenticLoopTable.stories.tsx @@ -13,8 +13,6 @@ export const Short: Story = { args: { duration: 4_200, toolCalls: 3, - inputTokens: 1234, - outputTokens: 567, }, }; @@ -22,8 +20,6 @@ export const Long: Story = { args: { duration: 125_000, toolCalls: 42, - inputTokens: 150_000, - outputTokens: 12_000, }, }; @@ -31,8 +27,6 @@ export const SingleToolCall: Story = { args: { duration: 980, toolCalls: 1, - inputTokens: 320, - outputTokens: 88, }, }; @@ -40,7 +34,5 @@ export const NoToolCalls: Story = { args: { duration: 500, toolCalls: 0, - inputTokens: 100, - outputTokens: 50, }, }; diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/AgenticLoopTable.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/AgenticLoopTable.tsx index 8578d9df93..e60f26a93e 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/AgenticLoopTable.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/AgenticLoopTable.tsx @@ -1,21 +1,16 @@ import type { FC } from "react"; import { cn } from "#/utils/cn"; -import { TokenBadges } from "../../TokenBadges"; import { roundDurationDisplay } from "../../utils"; interface AgenticLoopTableProps { duration: number; // in seconds toolCalls: number; - inputTokens: number; - outputTokens: number; className?: string; } export const AgenticLoopTable: FC = ({ duration, toolCalls, - inputTokens, - outputTokens, className, }) => { return ( @@ -25,10 +20,6 @@ export const AgenticLoopTable: FC = ({ className, )} > -
- In / out tokens - -
Tool calls {toolCalls} diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/PromptTable.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/PromptTable.tsx index ecf8fb0f68..be4c74a6e8 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/PromptTable.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/PromptTable.tsx @@ -43,7 +43,7 @@ export const PromptTable: FC = ({ className="text-right flex items-center justify-end" title={formatDate(timestamp)} > - + {formatDate(timestamp)} diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.tsx index 46550a9b5e..3c724ce7d6 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.tsx @@ -30,14 +30,17 @@ import { AgenticLoopTable } from "./AgenticLoopTable"; import { PromptTable } from "./PromptTable"; import { ToolCallTable } from "./ToolCallTable"; -const EXPANDABLE_COLLAPSE_HEIGHT = 50; - interface ExpandableTextProps { + maxHeight: number; text: string; className?: string; } -const ExpandableText: FC = ({ text, className }) => { +const ExpandableText: FC = ({ + maxHeight, + text, + className, +}) => { const contentRef = useRef(null); const [isExpandable, setIsExpandable] = useState(false); const [isExpanded, setIsExpanded] = useState(false); @@ -45,8 +48,8 @@ const ExpandableText: FC = ({ text, className }) => { useEffect(() => { const el = contentRef.current; if (!el) return; - setIsExpandable(el.scrollHeight > EXPANDABLE_COLLAPSE_HEIGHT); - }, []); + setIsExpandable(el.scrollHeight > maxHeight); + }, [maxHeight]); return (
@@ -55,11 +58,11 @@ const ExpandableText: FC = ({ text, className }) => { style={ isExpandable && !isExpanded ? { - maxHeight: EXPANDABLE_COLLAPSE_HEIGHT, + maxHeight, } : undefined } - className={cn(className, "overflow-hidden", isExpanded && "pb-9")} + className={cn(className, "overflow-scroll", isExpanded && "pb-9")} > {text}

@@ -161,6 +164,7 @@ const ThinkingBlock: FC = ({ text }) => ( Thinking...
@@ -210,8 +214,11 @@ const ToolCallBlock: FC = ({ outputTokens={outputTokens} tokenUsageMetadata={tokenUsageMetadata} /> -
-						{tool} 
+					
+						{tool}
+						
+							
+						
 					
)} @@ -298,7 +305,9 @@ const ThreadItem: FC = ({ thread, initiator }) => { new Date(thread.ended_at ?? Date.now()).getTime() - new Date(thread.started_at).getTime(); - const toolCalls = thread.agentic_actions?.reduce( + const hasAgenticLoop = thread.agentic_actions.length > 0; + + const toolCalls = thread.agentic_actions.reduce( (count, action) => count + action.tool_calls.length, 0, ); @@ -320,7 +329,7 @@ const ThreadItem: FC = ({ thread, initiator }) => {
{/* center column: prompt */} -
+
{thread.prompt && ( <>
@@ -352,9 +361,11 @@ const ThreadItem: FC = ({ thread, initiator }) => {
-

- {thread.prompt} -

+ )}
@@ -369,50 +380,54 @@ const ThreadItem: FC = ({ thread, initiator }) => { />
- - {/* Agentic loop */} -
-
- setAgenticLoopOpen(!agenticLoopOpen)} - > - Agentic loop - -
- - -
- - {agenticLoopOpen && ( - <> - {/* the little top rounded line above the thinking block */} -
- {/* we need the 1px extra to line up with the left border on the other lines */} + {hasAgenticLoop ? ( + + {/* Agentic loop */} +
+
+ setAgenticLoopOpen(!agenticLoopOpen)} + > + Agentic loop +
- {/* Agentic actions */} - {thread.agentic_actions?.map((action, i) => ( - - ))} - - {/* Agentic loop completed block */} - - - )} - +
+ + {agenticLoopOpen && ( + <> + {/* the little top rounded line above the thinking block */} +
+ {/* we need the 1px extra to line up with the left border on the other lines */} +
+ + {/* Agentic actions */} + {thread.agentic_actions?.map((action, i) => ( + + ))} + + {/* Agentic loop completed block */} + + + )} +
+ ) : ( + // if no agentic loop, we need a little spacing element to create + // the visual gap between threads +
+ )} ); }; @@ -524,7 +539,7 @@ export const SessionTimeline: FC = ({
{/* vertical line */}
-
+
{/* horizontal border */}
diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimelineSkeleton.stories.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimelineSkeleton.stories.tsx new file mode 100644 index 0000000000..f68dda3e90 --- /dev/null +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimelineSkeleton.stories.tsx @@ -0,0 +1,12 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { SessionTimelineSkeleton } from "./SessionTimelineSkeleton"; + +const meta: Meta = { + title: "pages/AIBridgePage/SessionTimeline/SessionTimelineSkeleton", + component: SessionTimelineSkeleton, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimelineSkeleton.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimelineSkeleton.tsx new file mode 100644 index 0000000000..4e53ca016b --- /dev/null +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimelineSkeleton.tsx @@ -0,0 +1,84 @@ +import type { FC } from "react"; +import { Skeleton } from "#/components/Skeleton/Skeleton"; +import { StatusIndicatorDot } from "#/components/StatusIndicator/StatusIndicator"; + +export const SessionTimelineSkeleton: FC = () => { + return ( +
+
+ {/* row 1: session start */} +
+ +
+
+ + Session started + +
+ + {/* row 2: vertical line */} +
+ + {/* row 3: space above timeline border */} +
+ + {/* row 4: top border */} +
+
+
+
+
+ + {/* row 5: skeleton thread cards */} +
+
+ {[0, 1, 2].map((i) => ( +
+ {/* avatar + username */} +
+ + +
+ {/* prompt */} +
+ + +
+ {/* right-column details */} +
+ + + +
+
+ ))} +
+
+ + {/* row 6: bottom border */} +
+
+
+
+
+ + {/* row 7: space below timeline border */} +
+ + {/* row 8: session end placeholder */} +
+ +
+
+ +
+
+
+ ); +}; diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/ToolCallTable.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/ToolCallTable.tsx index 061706c10b..a0ee67619f 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/ToolCallTable.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/ToolCallTable.tsx @@ -39,7 +39,7 @@ export const ToolCallTable: FC = ({
Started at {formatDate(timestamp)}