UI update with sidebar and settings tabs (#3288)

Co-authored-by: Nahiyan Khan <nahiyan@squareup.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Lily Delalande <119957291+lily-de@users.noreply.github.com>
Co-authored-by: Spence <spencrmartin@gmail.com>
Co-authored-by: spencrmartin <spencermartin@squareup.com>
Co-authored-by: Judson Stephenson <Jud@users.noreply.github.com>
Co-authored-by: Max Novich <mnovich@squareup.com>
Co-authored-by: Best Codes <106822363+The-Best-Codes@users.noreply.github.com>
Co-authored-by: caroline-a-mckenzie <cmckenzie@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Zane
2025-07-15 17:24:41 -07:00
committed by GitHub
parent b22f50d1a1
commit 77ea27f5f5
195 changed files with 20633 additions and 7238 deletions
@@ -1,18 +1,19 @@
import React, { useEffect, useRef } from 'react';
import { Card } from './ui/card';
import React, { useEffect, useRef, useState } from 'react';
import { Button } from './ui/button';
import { ToolCallArguments, ToolCallArgumentValue } from './ToolCallArguments';
import MarkdownContent from './MarkdownContent';
import { Content, ToolRequestMessageContent, ToolResponseMessageContent } from '../types/message';
import { snakeToTitleCase } from '../utils';
import { cn, snakeToTitleCase } from '../utils';
import Dot, { LoadingStatus } from './ui/Dot';
import Expand from './ui/Expand';
import { NotificationEvent } from '../hooks/useMessageStream';
import { ChevronRight, LoaderCircle } from 'lucide-react';
interface ToolCallWithResponseProps {
isCancelledMessage: boolean;
toolRequest: ToolRequestMessageContent;
toolResponse?: ToolResponseMessageContent;
notifications?: NotificationEvent[];
isStreamingMessage?: boolean;
}
export default function ToolCallWithResponse({
@@ -20,6 +21,7 @@ export default function ToolCallWithResponse({
toolRequest,
toolResponse,
notifications,
isStreamingMessage = false,
}: ToolCallWithResponseProps) {
const toolCall = toolRequest.toolCall.status === 'success' ? toolRequest.toolCall.value : null;
if (!toolCall) {
@@ -27,10 +29,14 @@ export default function ToolCallWithResponse({
}
return (
<div className={'w-full text-textSubtle text-sm'}>
<Card className="">
<ToolCallView {...{ isCancelledMessage, toolCall, toolResponse, notifications }} />
</Card>
<div
className={cn(
'w-full text-sm rounded-lg overflow-hidden border-borderSubtle border bg-background-muted'
)}
>
<ToolCallView
{...{ isCancelledMessage, toolCall, toolResponse, notifications, isStreamingMessage }}
/>
</div>
);
}
@@ -59,10 +65,19 @@ function ToolCallExpandable({
return (
<div className={className}>
<button onClick={toggleExpand} className="w-full flex justify-between items-center pr-2">
<span className="flex items-center">{label}</span>
<Expand size={5} isExpanded={isExpanded} />
</button>
<Button
onClick={toggleExpand}
className="group w-full flex justify-between items-center pr-2 transition-colors rounded-none"
variant="ghost"
>
<span className="flex items-center font-mono">{label}</span>
<ChevronRight
className={cn(
'group-hover:opacity-100 transition-transform opacity-70',
isExpanded && 'rotate-90'
)}
/>
</Button>
{isExpanded && <div>{children}</div>}
</div>
);
@@ -76,6 +91,7 @@ interface ToolCallViewProps {
};
toolResponse?: ToolResponseMessageContent;
notifications?: NotificationEvent[];
isStreamingMessage?: boolean;
}
interface Progress {
@@ -110,8 +126,28 @@ function ToolCallView({
toolCall,
toolResponse,
notifications,
isStreamingMessage = false,
}: ToolCallViewProps) {
const responseStyle = localStorage.getItem('response_style');
const [responseStyle, setResponseStyle] = useState(() => localStorage.getItem('response_style'));
// Listen for localStorage changes to update the response style
useEffect(() => {
const handleStorageChange = () => {
setResponseStyle(localStorage.getItem('response_style'));
};
// Listen for storage events (changes from other tabs/windows)
window.addEventListener('storage', handleStorageChange);
// Listen for custom events (changes from same tab)
window.addEventListener('responseStyleChanged', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('responseStyleChanged', handleStorageChange);
};
}, []);
const isExpandToolDetails = (() => {
switch (responseStyle) {
case 'concise':
@@ -123,9 +159,27 @@ function ToolCallView({
})();
const isToolDetails = Object.entries(toolCall?.arguments).length > 0;
const loadingStatus: LoadingStatus = !toolResponse?.toolResult.status
? 'loading'
: toolResponse?.toolResult.status;
// Check if streaming has finished but no tool response was received
// This is a workaround for cases where the backend doesn't send tool responses
const isStreamingComplete = !isStreamingMessage;
const shouldShowAsComplete = isStreamingComplete && !toolResponse;
const loadingStatus: LoadingStatus = !toolResponse
? shouldShowAsComplete
? 'success'
: 'loading'
: toolResponse.toolResult.status;
// Tool call timing tracking
const [startTime, setStartTime] = useState<number | null>(null);
// Track when tool call starts (when there's no response yet)
useEffect(() => {
if (!toolResponse && startTime === null) {
setStartTime(Date.now());
}
}, [toolResponse, startTime]);
const toolResults: { result: Content; isExpandToolResults: boolean }[] =
loadingStatus === 'success' && Array.isArray(toolResponse?.toolResult.value)
@@ -134,10 +188,17 @@ function ToolCallView({
const audience = item.annotations?.audience as string[] | undefined;
return !audience || audience.includes('user');
})
.map((item) => ({
result: item,
isExpandToolResults: ((item.annotations?.priority as number | undefined) ?? -1) >= 0.5,
}))
.map((item) => {
// Use user preference for detailed/concise, but still respect high priority items
const priority = (item.annotations?.priority as number | undefined) ?? -1;
const isHighPriority = priority >= 0.5;
const shouldExpandBasedOnStyle = responseStyle === 'detailed' || responseStyle === null;
return {
result: item,
isExpandToolResults: isHighPriority || shouldExpandBasedOnStyle,
};
})
: [];
const logs = notifications
@@ -163,11 +224,19 @@ function ToolCallView({
const isRenderingProgress =
loadingStatus === 'loading' && (progressEntries.length > 0 || (logs || []).length > 0);
// Only expand if there are actual results that need to be shown, not just for tool details
const isShouldExpand = toolResults.some((v) => v.isExpandToolResults);
// Determine if the main tool call should be expanded
const isShouldExpand = (() => {
// Always expand if there are high priority results that need to be shown
const hasHighPriorityResults = toolResults.some((v) => v.isExpandToolResults);
// Also expand based on user preference for detailed mode
const shouldExpandBasedOnStyle = responseStyle === 'detailed' || responseStyle === null;
return hasHighPriorityResults || shouldExpandBasedOnStyle;
})();
// Function to create a descriptive representation of what the tool is doing
const getToolDescription = () => {
const getToolDescription = (): string | null => {
const args = toolCall.arguments as Record<string, ToolCallArgumentValue>;
const toolName = toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2);
@@ -318,8 +387,14 @@ function ToolCallView({
isForceExpand={isShouldExpand}
label={
<>
<Dot size={2} loadingStatus={loadingStatus} />
<span className="ml-[10px]">
<div className="w-2 flex items-center justify-center">
{loadingStatus === 'loading' ? (
<LoaderCircle className="animate-spin text-text-muted" size={3} />
) : (
<Dot size={2} loadingStatus={loadingStatus} />
)}
</div>
<span className="ml-2">
{(() => {
const description = getToolDescription();
if (description) {
@@ -334,17 +409,19 @@ function ToolCallView({
>
{/* Tool Details */}
{isToolDetails && (
<div className="bg-bgStandard rounded-t mt-1">
<div className="border-t border-borderSubtle">
<ToolDetailsView toolCall={toolCall} isStartExpanded={isExpandToolDetails} />
</div>
)}
{logs && logs.length > 0 && (
<div className="bg-bgStandard mt-1">
<div className="border-t border-borderSubtle">
<ToolLogsView
logs={logs}
working={toolResults.length === 0}
isStartExpanded={toolResults.length === 0}
working={loadingStatus === 'loading'}
isStartExpanded={
loadingStatus === 'loading' || responseStyle === 'detailed' || responseStyle === null
}
/>
</div>
)}
@@ -352,7 +429,7 @@ function ToolCallView({
{toolResults.length === 0 &&
progressEntries.length > 0 &&
progressEntries.map((entry, index) => (
<div className="p-2" key={index}>
<div className="p-3 border-t border-borderSubtle" key={index}>
<ProgressBar progress={entry.progress} total={entry.total} message={entry.message} />
</div>
))}
@@ -361,15 +438,8 @@ function ToolCallView({
{!isCancelledMessage && (
<>
{toolResults.map(({ result, isExpandToolResults }, index) => {
const isLast = index === toolResults.length - 1;
return (
<div
key={index}
className={`bg-bgStandard mt-1
${isToolDetails || index > 0 ? '' : 'rounded-t'}
${isLast ? 'rounded-b' : ''}
`}
>
<div key={index} className={cn('border-t border-borderSubtle')}>
<ToolResultView result={result} isStartExpanded={isExpandToolResults} />
</div>
);
@@ -391,13 +461,14 @@ interface ToolDetailsViewProps {
function ToolDetailsView({ toolCall, isStartExpanded }: ToolDetailsViewProps) {
return (
<ToolCallExpandable
label="Tool Details"
className="pl-[19px] py-1"
label={<span className="pl-4 font-medium">Tool Details</span>}
isStartExpanded={isStartExpanded}
>
{toolCall.arguments && (
<ToolCallArguments args={toolCall.arguments as Record<string, ToolCallArgumentValue>} />
)}
<div className="pr-4 pl-8">
{toolCall.arguments && (
<ToolCallArguments args={toolCall.arguments as Record<string, ToolCallArgumentValue>} />
)}
</div>
</ToolCallExpandable>
);
}
@@ -410,14 +481,14 @@ interface ToolResultViewProps {
function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) {
return (
<ToolCallExpandable
label={<span className="pl-[19px] py-1">Output</span>}
label={<span className="pl-4 py-1 font-medium">Output</span>}
isStartExpanded={isStartExpanded}
>
<div className="bg-bgApp rounded-b pl-[19px] pr-2 py-4">
<div className="pl-4 pr-4 py-4">
{result.type === 'text' && result.text && (
<MarkdownContent
content={result.text}
className="whitespace-pre-wrap p-2 max-w-full overflow-x-auto"
className="whitespace-pre-wrap max-w-full overflow-x-auto"
/>
)}
{result.type === 'image' && (
@@ -457,7 +528,7 @@ function ToolLogsView({
return (
<ToolCallExpandable
label={
<span className="pl-[19px] py-1">
<span className="pl-4 py-1 font-medium flex items-center">
<span>Logs</span>
{working && (
<div className="mx-2 inline-block">
@@ -475,7 +546,7 @@ function ToolLogsView({
>
<div
ref={boxRef}
className={`flex flex-col items-start space-y-2 overflow-y-auto ${working ? 'max-h-[4rem]' : 'max-h-[20rem]'} bg-bgApp`}
className={`flex flex-col items-start space-y-2 overflow-y-auto p-4 ${working ? 'max-h-[4rem]' : 'max-h-[20rem]'}`}
>
{logs.map((log, i) => (
<span key={i} className="font-mono text-sm text-textSubtle">
@@ -493,16 +564,16 @@ const ProgressBar = ({ progress, total, message }: Omit<Progress, 'progressToken
return (
<div className="w-full space-y-2">
{message && <div className="text-sm text-gray-700">{message}</div>}
{message && <div className="text-sm text-textSubtle">{message}</div>}
<div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden relative">
<div className="w-full bg-background-subtle rounded-full h-4 overflow-hidden relative">
{isDeterminate ? (
<div
className="bg-blue-500 h-full transition-all duration-300"
className="bg-primary h-full transition-all duration-300"
style={{ width: `${percent}%` }}
/>
) : (
<div className="absolute inset-0 animate-indeterminate bg-blue-500" />
<div className="absolute inset-0 animate-indeterminate bg-primary" />
)}
</div>
</div>