Skip to content

Commit e02d3d5

Browse files
wsxiaoyspochi-agentliangfung
authored
feat(vscode-webui): improve error message handling in chat UI (#563)
* feat(vscode-webui): improve error message handling in chat UI - Move ErrorMessageView to ChatToolbar for better positioning - Add proper spacing for error messages with absolute positioning - Update component imports and layout adjustments 🤖 Generated with [Pochi](https://getpochi.com) Co-Authored-By: Pochi <[email protected]> * update * update: bg color * update --------- Co-authored-by: Pochi <[email protected]> Co-authored-by: liangfung <[email protected]>
1 parent 456aef9 commit e02d3d5

File tree

6 files changed

+132
-21
lines changed

6 files changed

+132
-21
lines changed
Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,127 @@
11
import { useIsDevMode } from "@/features/settings";
22
import { cn } from "@/lib/utils";
3+
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
34
import type { ReactNode } from "react";
5+
import { useCallback, useEffect, useRef, useState } from "react";
46
import { ScrollArea } from "./ui/scroll-area";
57

68
interface ErrorMessageProps {
79
error: { message: string } | undefined;
810
formatter?: (e: { message: string }) => ReactNode;
11+
collapsible?: boolean;
12+
viewportClassname?: string;
913
}
1014

1115
export const ErrorMessage: React.FC<ErrorMessageProps> = ({
1216
error,
1317
formatter,
18+
collapsible = false,
19+
viewportClassname,
1420
}) => {
1521
const [isDevMode] = useIsDevMode();
22+
const [isExpanded, setIsExpanded] = useState(false);
23+
const [hasOverflow, setHasOverflow] = useState(false);
24+
const contentRef = useRef<HTMLDivElement>(null);
25+
26+
const errorMessage = error?.message;
27+
28+
const checkOverflow = useCallback(() => {
29+
if (collapsible && contentRef.current) {
30+
const lineHeight = Number.parseFloat(
31+
window.getComputedStyle(contentRef.current).lineHeight || "20",
32+
);
33+
const height = contentRef.current.scrollHeight;
34+
setHasOverflow(height > lineHeight * 1.5);
35+
}
36+
}, [collapsible]);
37+
38+
// biome-ignore lint/correctness/useExhaustiveDependencies: Need to reset state when error changes
39+
useEffect(() => {
40+
checkOverflow();
41+
setIsExpanded(false);
42+
}, [errorMessage, checkOverflow]);
43+
44+
useEffect(() => {
45+
if (!collapsible || !contentRef.current) {
46+
return;
47+
}
48+
49+
const resizeObserver = new ResizeObserver(() => {
50+
checkOverflow();
51+
});
52+
53+
resizeObserver.observe(contentRef.current);
54+
55+
return () => {
56+
resizeObserver.disconnect();
57+
};
58+
}, [collapsible, checkOverflow]);
59+
60+
const handleToggle = () => {
61+
if (hasOverflow) {
62+
setIsExpanded(!isExpanded);
63+
}
64+
};
65+
66+
const isCollapsed = collapsible && hasOverflow && !isExpanded;
67+
1668
return (
1769
error && (
18-
<ScrollArea
19-
className={cn("mb-2 break-all text-center text-error", {
20-
"cursor-help": isDevMode,
21-
})}
22-
viewportClassname="max-h-32"
23-
onClick={isDevMode ? () => console.error(error) : undefined}
24-
>
25-
{formatter ? formatter(error) : error.message}
26-
</ScrollArea>
70+
<div className="relative mb-2">
71+
{(() => {
72+
const content = (
73+
<div ref={contentRef}>
74+
{formatter ? formatter(error) : error.message}
75+
</div>
76+
);
77+
78+
return isCollapsed ? (
79+
<div
80+
className={cn(
81+
"max-h-6 overflow-hidden break-all pr-8 text-center text-error",
82+
{
83+
"cursor-help": isDevMode,
84+
},
85+
)}
86+
onClick={isDevMode ? () => console.error(error) : undefined}
87+
>
88+
{content}
89+
</div>
90+
) : (
91+
<ScrollArea
92+
className={cn("break-all text-center text-error", {
93+
"cursor-help": isDevMode && (!collapsible || !hasOverflow),
94+
"pr-8": collapsible && hasOverflow,
95+
})}
96+
viewportClassname={cn(
97+
"max-h-[max(1.5rem,calc(100vh-38rem))]",
98+
viewportClassname,
99+
)}
100+
onClick={
101+
isDevMode && (!collapsible || !hasOverflow)
102+
? () => console.error(error)
103+
: undefined
104+
}
105+
>
106+
{content}
107+
</ScrollArea>
108+
);
109+
})()}
110+
{collapsible && hasOverflow && (
111+
<button
112+
type="button"
113+
onClick={handleToggle}
114+
className="absolute top-1.5 right-2 transition-opacity hover:opacity-80"
115+
aria-label={isExpanded ? "Collapse" : "Expand"}
116+
>
117+
{isExpanded ? (
118+
<ChevronUpIcon className="size-4" />
119+
) : (
120+
<ChevronDownIcon className="size-4" />
121+
)}
122+
</button>
123+
)}
124+
</div>
27125
)
28126
);
29127
};

packages/vscode-webui/src/features/chat/components/chat-area.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ interface ChatAreaProps {
99
isLoading: boolean;
1010
user?: { name: string; image?: string | null };
1111
messagesContainerRef: React.RefObject<HTMLDivElement | null>;
12+
className?: string;
1213
}
1314

1415
export function ChatArea({
1516
messages,
1617
isLoading,
1718
user,
1819
messagesContainerRef,
20+
className,
1921
}: ChatAreaProps) {
2022
const resourceUri = useResourceURI();
2123
return (
@@ -31,6 +33,7 @@ export function ChatArea({
3133
}}
3234
isLoading={isLoading}
3335
containerRef={messagesContainerRef}
36+
className={className}
3437
/>
3538
</>
3639
);

packages/vscode-webui/src/features/chat/components/chat-toolbar.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { useInlineCompactTask } from "../hooks/use-inline-compact-task";
3232
import { useNewCompactTask } from "../hooks/use-new-compact-task";
3333
import type { SubtaskInfo } from "../hooks/use-subtask-info";
3434
import { ChatInputForm } from "./chat-input-form";
35+
import { ErrorMessageView } from "./error-message-view";
3536
import { CompleteSubtaskButton } from "./subtask";
3637

3738
interface ChatToolbarProps {
@@ -189,14 +190,19 @@ export const ChatToolbar: React.FC<ChatToolbarProps> = ({
189190

190191
return (
191192
<>
192-
<CompleteSubtaskButton subtask={subtask} messages={messages} />
193-
<ApprovalButton
194-
pendingApproval={pendingApproval}
195-
retry={retry}
196-
allowAddToolResult={allowAddToolResult}
197-
isSubTask={isSubTask}
198-
task={task}
199-
/>
193+
<div className="-translate-y-full -top-2 absolute left-0 w-full px-4 pt-1">
194+
<div className="bg-background">
195+
<ErrorMessageView error={displayError} />
196+
<CompleteSubtaskButton subtask={subtask} messages={messages} />
197+
<ApprovalButton
198+
pendingApproval={pendingApproval}
199+
retry={retry}
200+
allowAddToolResult={allowAddToolResult}
201+
isSubTask={isSubTask}
202+
task={task}
203+
/>
204+
</div>
205+
</div>
200206
{todos && todos.length > 0 && (
201207
<TodoList todos={todos} className="mt-2">
202208
<TodoList.Header />

packages/vscode-webui/src/features/chat/components/error-message-view.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function ErrorMessageView({
1818
return (
1919
<ErrorMessage
2020
error={debouncedError}
21+
collapsible
2122
formatter={(e) => {
2223
if (e.message === PochiApiErrors.ReachedCreditLimit) {
2324
return (

packages/vscode-webui/src/features/chat/page.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { useTranslation } from "react-i18next";
2424

2525
import { useLatest } from "@/lib/hooks/use-latest";
2626
import { useMcp } from "@/lib/hooks/use-mcp";
27+
import { cn } from "@/lib/utils";
2728
import { useApprovalAndRetry } from "../approval";
2829
import { getReadyForRetryError } from "../retry/hooks/use-ready-for-retry-error";
2930
import {
@@ -37,7 +38,6 @@ import {
3738
} from "../settings/hooks/use-tool-auto-approval";
3839
import { ChatArea } from "./components/chat-area";
3940
import { ChatToolbar } from "./components/chat-toolbar";
40-
import { ErrorMessageView } from "./components/error-message-view";
4141
import { SubtaskHeader } from "./components/subtask";
4242
import { useRestoreTaskModel } from "./hooks/use-restore-task-model";
4343
import { useScrollToBottom } from "./hooks/use-scroll-to-bottom";
@@ -326,9 +326,12 @@ function Chat({ user, uid, prompt, files }: ChatProps) {
326326
isLoading={isLoading}
327327
user={user || defaultUser}
328328
messagesContainerRef={messagesContainerRef}
329+
className={cn({
330+
// Leave more space for errors as errors / approval button are absolutely positioned
331+
"pb-14": !!displayError,
332+
})}
329333
/>
330-
<div className="flex flex-col px-4">
331-
<ErrorMessageView error={displayError} />
334+
<div className="relative flex flex-col px-4">
332335
{!isWorkspaceActive ? (
333336
<WorkspaceRequiredPlaceholder
334337
isFetching={isFetchingWorkspace}

packages/vscode-webui/src/features/settings/components/auto-approve-menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export function AutoApproveMenu({ isSubTask }: { isSubTask: boolean }) {
169169
<PopoverTrigger asChild>
170170
<div
171171
className={cn(
172-
"relative mt-2 flex cursor-pointer select-none items-center justify-between py-2.5",
172+
"relative flex cursor-pointer select-none items-center justify-between py-2.5",
173173
)}
174174
>
175175
<div className="flex w-full overflow-x-hidden">

0 commit comments

Comments
 (0)