authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-03 22:35:24-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 22:35:24-04:00
log710f6dc417ebf303eede3df3605a6938482d83ab
tree0f82a138475a2e8c9ad88d99d45775522bdb547b
parentf96a220b5b154ea44c94bf43929c6362cd511699
signature Signed by PGP key B5690EEEBB952194

fix(web): simplify expanded tool details (#9549)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

1 files changed, 65 insertions(+), 27 deletions(-)

apps/web/src/components/chat/MessagesTimeline.tsx+65-27
...@@ -2886,27 +2886,47 @@ function workEntryRawCommand(...@@ -2886,27 +2886,47 @@ function workEntryRawCommand(
2886function buildToolCallExpandedBody(2886function buildToolCallExpandedBody(
2887 workEntry: TimelineWorkEntry,2887 workEntry: TimelineWorkEntry,
2888 workspaceRoot: string | undefined,2888 workspaceRoot: string | undefined,
2889 visibleLabel: string,
2890 viewedImagePath: string | null,
2889): string | null {2891): string | null {
2890 const blocks: string[] = [];2892 const blocks: string[] = [];
2893 const seen = new Set<string>();
2894 const addBlock = (value: string | null | undefined) => {
2895 const text = value?.trim();
2896 if (!text || seen.has(text)) return;
2897 seen.add(text);
2898 blocks.push(text);
2899 };
2891 if (workEntry.itemType === "mcp_tool_call" && workEntry.toolData !== undefined) {2900 if (workEntry.itemType === "mcp_tool_call" && workEntry.toolData !== undefined) {
2892 blocks.push(`MCP call\n${JSON.stringify(workEntry.toolData, null, 2)}`);2901 addBlock(`MCP call\n${JSON.stringify(workEntry.toolData, null, 2)}`);
2893 }2902 }
2903 const command = workEntry.command?.trim();
2894 const raw = workEntryRawCommand(workEntry);2904 const raw = workEntryRawCommand(workEntry);
2895 if (raw?.trim()) {2905 if (command === visibleLabel.trim()) {
2896 blocks.push(raw.trim());2906 seen.add(command);
2897 } else if (workEntry.command?.trim()) {2907 } else {
2898 blocks.push(workEntry.command.trim());2908 addBlock(raw ?? command);
2899 }2909 }
2900 if (workEntry.detail?.trim()) {2910 const detail = workEntry.detail?.trim();
2901 blocks.push(workEntry.detail.trim());2911 if (detail !== viewedImagePath?.trim()) {
2912 addBlock(detail);
2902 }2913 }
2903 const changedFiles = workEntry.changedFiles ?? [];2914 const viewedImagePaths = new Set(
2915 viewedImagePath
2916 ? [viewedImagePath.trim(), formatWorkspaceRelativePath(viewedImagePath, workspaceRoot)]
2917 : [],
2918 );
2919 const changedFiles = (workEntry.changedFiles ?? []).flatMap((filePath) => {
2920 const formattedPath = formatWorkspaceRelativePath(filePath, workspaceRoot);
2921 return viewedImagePaths.has(filePath) ||
2922 viewedImagePaths.has(formattedPath) ||
2923 filePath.trim() === detail ||
2924 formattedPath === detail
2925 ? []
2926 : [formattedPath];
2927 });
2904 if (changedFiles.length > 0) {2928 if (changedFiles.length > 0) {
2905 blocks.push(2929 addBlock([...new Set(changedFiles)].join("\n"));
2906 changedFiles
2907 .map((filePath) => formatWorkspaceRelativePath(filePath, workspaceRoot))
2908 .join("\n"),
2909 );
2910 }2930 }
2911 return blocks.length > 0 ? blocks.join("\n\n") : null;2931 return blocks.length > 0 ? blocks.join("\n\n") : null;
2912}2932}
...@@ -3092,19 +3112,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {...@@ -3092,19 +3112,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
3092 ? "circle-alert"3112 ? "circle-alert"
3093 : workEntryIconName(workEntry);3113 : workEntryIconName(workEntry);
3094 const previewText = displayLabel ?? workEntryDisplayLabel(workEntry, workspaceRoot);3114 const previewText = displayLabel ?? workEntryDisplayLabel(workEntry, workspaceRoot);
3095 const displayText =
3096 !toolPresentation && expanded && workEntry.command?.trim() ? "Command" : previewText;
3097 const viewedImagePath = workEntryViewedImagePath(workEntry);3115 const viewedImagePath = workEntryViewedImagePath(workEntry);
3098 const canExpand =
3099 (workEntry.itemType === "mcp_tool_call" && workEntry.toolData !== undefined) ||
3100 Boolean(
3101 workEntryRawCommand(workEntry) ||
3102 workEntry.command?.trim() ||
3103 workEntry.detail?.trim() ||
3104 workEntry.changedFiles?.length ||
3105 viewedImagePath,
3106 );
3107 const expandedBody = expanded ? buildToolCallExpandedBody(workEntry, workspaceRoot) : null;
3108 const viewedImage =3116 const viewedImage =
3109 viewedImagePath && threadRef3117 viewedImagePath && threadRef
3110 ? resolveViewedImageAsset(viewedImagePath, {3118 ? resolveViewedImageAsset(viewedImagePath, {
...@@ -3112,6 +3120,24 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {...@@ -3112,6 +3120,24 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
3112 workspaceRoot,3120 workspaceRoot,
3113 })3121 })
3114 : null;3122 : null;
3123 const commandMatchesVisibleLabel = workEntry.command?.trim() === previewText.trim();
3124 const canExpand =
3125 (workEntry.itemType === "mcp_tool_call" && workEntry.toolData !== undefined) ||
3126 Boolean(
3127 (!commandMatchesVisibleLabel &&
3128 (workEntryRawCommand(workEntry) || workEntry.command?.trim())) ||
3129 workEntry.detail?.trim() ||
3130 workEntry.changedFiles?.length ||
3131 viewedImage,
3132 );
3133 const expandedBody = expanded
3134 ? buildToolCallExpandedBody(
3135 workEntry,
3136 workspaceRoot,
3137 previewText,
3138 viewedImage ? viewedImagePath : null,
3139 )
3140 : null;
3115 const showDestructiveRowStyle =3141 const showDestructiveRowStyle =
3116 showFailedIndicator &&3142 showFailedIndicator &&
3117 (workEntrySignalsSevereFailure(workEntry) || !workLogEntryIsToolLike(workEntry));3143 (workEntrySignalsSevereFailure(workEntry) || !workLogEntryIsToolLike(workEntry));
...@@ -3183,7 +3209,19 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {...@@ -3183,7 +3209,19 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
3183 <div className="flex min-w-0 flex-1 items-center gap-1.5">3209 <div className="flex min-w-0 flex-1 items-center gap-1.5">
3184 <div className="min-w-0 flex-1 overflow-hidden">3210 <div className="min-w-0 flex-1 overflow-hidden">
3185 <p className="flex min-w-0 w-full items-baseline gap-1.5 text-sm leading-relaxed">3211 <p className="flex min-w-0 w-full items-baseline gap-1.5 text-sm leading-relaxed">
3186 <span className={cn("min-w-0 flex-1 truncate", headingClass)}>{displayText}</span>3212 <span
3213 className={cn(
3214 "min-w-0 flex-1",
3215 expanded || (commandMatchesVisibleLabel && !canExpand)
3216 ? "whitespace-pre-wrap break-words select-text"
3217 : "truncate",
3218 headingClass,
3219 )}
3220 onClick={expanded ? stopRowToggle : undefined}
3221 onPointerDown={expanded ? stopRowToggle : undefined}
3222 >
3223 {previewText}
3224 </span>
3187 </p>3225 </p>
3188 </div>3226 </div>
3189 {showFailedIndicator && hasSpecialToolIcon ? (3227 {showFailedIndicator && hasSpecialToolIcon ? (
...@@ -3224,7 +3262,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {...@@ -3224,7 +3262,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
3224 ) : null}3262 ) : null}
3225 {expanded && canExpand && expandedBody ? (3263 {expanded && canExpand && expandedBody ? (
3226 <div3264 <div
3227 className="mt-1 ms-7 cursor-default border-s border-border/45 ps-3 pt-0.5"3265 className="mt-1 ms-7 cursor-default rounded-md bg-muted/40 px-3 py-2"
3228 onClick={stopRowToggle}3266 onClick={stopRowToggle}
3229 onPointerDown={stopRowToggle}3267 onPointerDown={stopRowToggle}
3230 >3268 >