From 2ea44966ec78fa36a068ea80fddceacca1dbca97 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Wed, 21 Jan 2026 23:51:20 +0800 Subject: [PATCH] feat(webui): enhance ChatViewer and components --- .../src/components/ChatViewer/ChatViewer.css | 18 +++-------- .../src/components/ChatViewer/ChatViewer.tsx | 27 ++++++++-------- .../src/components/toolcalls/EditToolCall.tsx | 18 ++++++++++- .../components/toolcalls/GenericToolCall.tsx | 12 ++++++- .../src/components/toolcalls/ReadToolCall.tsx | 18 ++++++++++- .../components/toolcalls/ShellToolCall.tsx | 32 ++++++++++++++++--- .../components/toolcalls/ThinkToolCall.tsx | 20 ++++++++++-- .../toolcalls/UpdatedPlanToolCall.tsx | 19 +++++++++-- .../components/toolcalls/WriteToolCall.tsx | 12 ++++++- .../toolcalls/shared/LayoutComponents.tsx | 2 +- packages/webui/src/styles/timeline.css | 31 +++++++++++------- 11 files changed, 157 insertions(+), 52 deletions(-) diff --git a/packages/webui/src/components/ChatViewer/ChatViewer.css b/packages/webui/src/components/ChatViewer/ChatViewer.css index c3a1a38bf..3d8144caf 100644 --- a/packages/webui/src/components/ChatViewer/ChatViewer.css +++ b/packages/webui/src/components/ChatViewer/ChatViewer.css @@ -82,15 +82,16 @@ gap: 0; align-items: flex-start; text-align: left; - padding: 8px 0; + padding-top: 8px; + padding-bottom: 8px; flex-direction: column; position: relative; animation: chatViewerFadeIn 0.2s ease-in; } -.chat-viewer-messages > *:not(:last-child) { - padding-bottom: 0; - padding-top: 0; +.chat-viewer-messages > .chat-viewer-scroll-anchor { + padding: 0; + display: block; } /* Disable overflow anchoring on individual items for manual scroll control */ @@ -103,15 +104,6 @@ margin-top: 0; } -/* =========================== - Timeline Wrapper Styles - =========================== */ - -/* Tool call wrapper - transparent container for timeline continuity */ -.chat-viewer-toolcall-wrapper { - position: relative; -} - /* =========================== Animations =========================== */ diff --git a/packages/webui/src/components/ChatViewer/ChatViewer.tsx b/packages/webui/src/components/ChatViewer/ChatViewer.tsx index 97570ab81..94a3c18b5 100644 --- a/packages/webui/src/components/ChatViewer/ChatViewer.tsx +++ b/packages/webui/src/components/ChatViewer/ChatViewer.tsx @@ -303,21 +303,17 @@ export const ChatViewer = forwardRef( if (msg.type === 'tool_call' && msg.toolCall) { const ToolCallComponent = getToolCallComponent(msg.toolCall.kind); + if (!ToolCallComponent) { + return null; + } + return ( -
- {ToolCallComponent && ( - - )} -
+ toolCall={msg.toolCall} + isFirst={isFirst} + isLast={isLast} + /> ); } @@ -380,7 +376,10 @@ export const ChatViewer = forwardRef( return (
-
+
{sortedMessages.length === 0 ? (
{showEmptyIcon && ( diff --git a/packages/webui/src/components/toolcalls/EditToolCall.tsx b/packages/webui/src/components/toolcalls/EditToolCall.tsx index 652c58166..fa3faf1c6 100644 --- a/packages/webui/src/components/toolcalls/EditToolCall.tsx +++ b/packages/webui/src/components/toolcalls/EditToolCall.tsx @@ -27,9 +27,13 @@ const EditToolCallContainer: React.FC = ({ toolCallId: _toolCallId, labelSuffix, className: _className, + isFirst = false, + isLast = false, }) => (
@@ -71,7 +75,11 @@ const getDiffSummary = ( * Specialized component for Edit tool calls * Optimized for displaying file editing operations with diffs */ -export const EditToolCall: React.FC = ({ toolCall }) => { +export const EditToolCall: React.FC = ({ + toolCall, + isFirst, + isLast, +}) => { const { content, locations, toolCallId } = toolCall; // Group content by type; memoize to avoid new array identities on every render @@ -85,6 +93,8 @@ export const EditToolCall: React.FC = ({ toolCall }) => { return (
@@ -118,6 +128,8 @@ export const EditToolCall: React.FC = ({ toolCall }) => { label={'Edit'} status="error" toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} labelSuffix={ path ? ( = ({ toolCall }) => { return (
@@ -175,6 +189,8 @@ export const EditToolCall: React.FC = ({ toolCall }) => { label={`Edit`} status={containerStatus} toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} labelSuffix={ = ({ toolCall }) => { +export const GenericToolCall: FC = ({ + toolCall, + isFirst, + isLast, +}) => { const { kind, title, content, locations, toolCallId } = toolCall; const operationText = safeTitle(title); @@ -94,6 +98,8 @@ export const GenericToolCall: FC = ({ toolCall }) => { label={getDisplayLabel()} status={statusFlag} toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} > {operationText || output} @@ -111,6 +117,8 @@ export const GenericToolCall: FC = ({ toolCall }) => { label={getDisplayLabel()} status={statusFlag} toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} > @@ -128,6 +136,8 @@ export const GenericToolCall: FC = ({ toolCall }) => { label={getDisplayLabel()} status={statusFlag} toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} > {operationText} diff --git a/packages/webui/src/components/toolcalls/ReadToolCall.tsx b/packages/webui/src/components/toolcalls/ReadToolCall.tsx index 6dc9673df..de2c0e388 100644 --- a/packages/webui/src/components/toolcalls/ReadToolCall.tsx +++ b/packages/webui/src/components/toolcalls/ReadToolCall.tsx @@ -30,9 +30,13 @@ const ReadToolCallContainer: FC = ({ toolCallId: _toolCallId, labelSuffix, className: _className, + isFirst = false, + isLast = false, }) => (
@@ -56,7 +60,11 @@ const ReadToolCallContainer: FC = ({ * ReadToolCall - displays file reading operations * Shows: Read filename (no content preview) */ -export const ReadToolCall: FC = ({ toolCall }) => { +export const ReadToolCall: FC = ({ + toolCall, + isFirst, + isLast, +}) => { const { content, locations, toolCallId } = toolCall; const platform = usePlatform(); const openedDiffsRef = useRef>(new Map()); @@ -137,6 +145,8 @@ export const ReadToolCall: FC = ({ toolCall }) => { className="read-tool-call-error" status="error" toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} labelSuffix={ path ? ( = ({ toolCall }) => { className="read-tool-call-error" status="error" toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} labelSuffix={ path ? ( = ({ toolCall }) => { className="read-tool-call-success" status={containerStatus} toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} labelSuffix={ path ? ( = ({ toolCall }) => { className="read-tool-call-success" status={containerStatus} toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} labelSuffix={ path ? ( = ({ toolCallId: _toolCallId, labelSuffix, className: _className, + isFirst = false, + isLast = false, }) => (
-
+
{label} @@ -95,6 +99,8 @@ const getInputCommand = ( const ShellToolCallImpl: FC = ({ toolCall, variant, + isFirst, + isLast, }) => { const { title, content, rawInput, toolCallId } = toolCall; const classPrefix = variant; @@ -157,7 +163,13 @@ const ShellToolCallImpl: FC = ({ // Error case if (errors.length > 0) { return ( - + {/* Branch connector summary */}
@@ -203,7 +215,13 @@ const ShellToolCallImpl: FC = ({ output.length > 500 ? output.substring(0, 500) + '...' : output; return ( - + {/* Branch connector summary */}
@@ -248,7 +266,13 @@ const ShellToolCallImpl: FC = ({ // Success without output: show command with branch connector return ( - +
= ({ toolCall }) => { +export const ThinkToolCall: FC = ({ + toolCall, + isFirst, + isLast, +}) => { const { content } = toolCall; // Group content by type @@ -29,7 +33,12 @@ export const ThinkToolCall: FC = ({ toolCall }) => { // Error case (rare for thinking) if (errors.length > 0) { return ( - + {errors.join('\n')} ); @@ -61,7 +70,12 @@ export const ThinkToolCall: FC = ({ toolCall }) => { ? 'loading' : 'default'; return ( - + {thoughts} ); diff --git a/packages/webui/src/components/toolcalls/UpdatedPlanToolCall.tsx b/packages/webui/src/components/toolcalls/UpdatedPlanToolCall.tsx index fcccfccfa..46dae47a1 100644 --- a/packages/webui/src/components/toolcalls/UpdatedPlanToolCall.tsx +++ b/packages/webui/src/components/toolcalls/UpdatedPlanToolCall.tsx @@ -27,9 +27,13 @@ const PlanToolCallContainer: FC = ({ toolCallId: _toolCallId, labelSuffix, className: _className, + isFirst = false, + isLast = false, }) => (
@@ -113,14 +117,23 @@ const parsePlanEntries = (textOutputs: string[]): PlanEntry[] => { * Specialized component for UpdatedPlan tool calls * Optimized for displaying plan update operations */ -export const UpdatedPlanToolCall: FC = ({ toolCall }) => { +export const UpdatedPlanToolCall: FC = ({ + toolCall, + isFirst, + isLast, +}) => { const { content, status } = toolCall; const { errors, textOutputs } = groupContent(content); // Error-first display if (errors.length > 0) { return ( - + {errors.join('\n')} ); @@ -134,6 +147,8 @@ export const UpdatedPlanToolCall: FC = ({ toolCall }) => { label={label} status={mapToolStatusToBullet(status)} className="update-plan-toolcall" + isFirst={isFirst} + isLast={isLast} >
    {entries.map((entry, idx) => { diff --git a/packages/webui/src/components/toolcalls/WriteToolCall.tsx b/packages/webui/src/components/toolcalls/WriteToolCall.tsx index ec2c6d1f0..861152ba1 100644 --- a/packages/webui/src/components/toolcalls/WriteToolCall.tsx +++ b/packages/webui/src/components/toolcalls/WriteToolCall.tsx @@ -19,7 +19,11 @@ import { FileLink } from '../layout/FileLink.js'; * Specialized component for Write tool calls * Shows: Write filename + error message + content preview */ -export const WriteToolCall: FC = ({ toolCall }) => { +export const WriteToolCall: FC = ({ + toolCall, + isFirst, + isLast, +}) => { const { content, locations, rawInput, toolCallId } = toolCall; // Group content by type @@ -50,6 +54,8 @@ export const WriteToolCall: FC = ({ toolCall }) => { label={'WriteFile'} status="error" toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} labelSuffix={ path ? ( = ({ toolCall }) => { label={'WriteFile'} status={containerStatus} toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} labelSuffix={ path ? ( = ({ toolCall }) => { label="WriteFile" status={containerStatus} toolCallId={toolCallId} + isFirst={isFirst} + isLast={isLast} > {textOutputs.join('\n')} diff --git a/packages/webui/src/components/toolcalls/shared/LayoutComponents.tsx b/packages/webui/src/components/toolcalls/shared/LayoutComponents.tsx index 3ef6e2877..19e3f92a8 100644 --- a/packages/webui/src/components/toolcalls/shared/LayoutComponents.tsx +++ b/packages/webui/src/components/toolcalls/shared/LayoutComponents.tsx @@ -53,7 +53,7 @@ export const ToolCallContainer: FC = ({ data-last={isLast} >
    -
    +
    {label} diff --git a/packages/webui/src/styles/timeline.css b/packages/webui/src/styles/timeline.css index 32e08c251..b69aeb815 100644 --- a/packages/webui/src/styles/timeline.css +++ b/packages/webui/src/styles/timeline.css @@ -37,22 +37,31 @@ .qwen-message.message-item:not(.user-message-container):first-child::after, .user-message-container + .qwen-message.message-item:not(.user-message-container)::after, -.chat-viewer-toolcall-wrapper:first-child .toolcall-container::after, -.user-message-container + .chat-viewer-toolcall-wrapper .toolcall-container::after { +.chat-messages + > :not(.qwen-message.message-item) + + .qwen-message.message-item:not(.user-message-container)::after, +.chat-viewer-messages + > :not(.qwen-message.message-item) + + .qwen-message.message-item:not(.user-message-container)::after { top: var(--timeline-center-offset, 13px); } .qwen-message.message-item:not(.user-message-container):has(+ .user-message-container)::after, .qwen-message.message-item:not(.user-message-container):has( - + :not(.qwen-message.message-item):not(.chat-viewer-toolcall-wrapper) + + :not(.qwen-message.message-item) )::after, -.qwen-message.message-item:not(.user-message-container):last-child::after, -.chat-viewer-toolcall-wrapper:has(+ .user-message-container) - .toolcall-container::after, -.chat-viewer-toolcall-wrapper:has( - + :not(.chat-viewer-toolcall-wrapper):not(.qwen-message.message-item) - ) - .toolcall-container::after, -.chat-viewer-toolcall-wrapper:last-child .toolcall-container::after { +.qwen-message.message-item:not(.user-message-container):last-child::after { bottom: calc(100% - var(--timeline-center-offset, 13px)); } + +.qwen-message.message-item[data-first='true']::after { + top: var(--timeline-center-offset, 13px); +} + +.qwen-message.message-item[data-last='true']::after { + bottom: calc(100% - var(--timeline-center-offset, 13px)); +} + +.qwen-message.message-item[data-first='true'][data-last='true']::after { + display: none; +}