| ... | @@ -11,6 +11,7 @@ import { | ... | @@ -11,6 +11,7 @@ import { |
| 11 | use, | 11 | use, |
| 12 | useCallback, | 12 | useCallback, |
| 13 | useEffect, | 13 | useEffect, |
| | 14 | useLayoutEffect, |
| 14 | useMemo, | 15 | useMemo, |
| 15 | useRef, | 16 | useRef, |
| 16 | useState, | 17 | useState, |
| ... | @@ -44,7 +45,6 @@ import { | ... | @@ -44,7 +45,6 @@ import { |
| 44 | EyeIcon, | 45 | EyeIcon, |
| 45 | GlobeIcon, | 46 | GlobeIcon, |
| 46 | HammerIcon, | 47 | HammerIcon, |
| 47 | type LucideIcon, | | |
| 48 | MessageCircleIcon, | 48 | MessageCircleIcon, |
| 49 | MinusIcon, | 49 | MinusIcon, |
| 50 | SquarePenIcon, | 50 | SquarePenIcon, |
| ... | @@ -396,7 +396,8 @@ const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: Time | ... | @@ -396,7 +396,8 @@ const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: Time |
| 396 | className={cn( | 396 | className={cn( |
| 397 | // Commentary (non-terminal assistant) rows carry no metadata row, so | 397 | // Commentary (non-terminal assistant) rows carry no metadata row, so |
| 398 | // they sit closer to the work that follows them. | 398 | // they sit closer to the work that follows them. |
| 399 | row.kind === "message" && row.message.role === "assistant" && !row.showAssistantMeta | 399 | (row.kind === "message" && row.message.role === "assistant" && !row.showAssistantMeta) || |
| | 400 | row.kind === "work" |
| 400 | ? "pb-2" | 401 | ? "pb-2" |
| 401 | : "pb-4", | 402 | : "pb-4", |
| 402 | row.kind === "message" && row.message.role === "assistant" ? "group/assistant" : null, | 403 | row.kind === "message" && row.message.role === "assistant" ? "group/assistant" : null, |
| ... | @@ -526,7 +527,7 @@ function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn- | ... | @@ -526,7 +527,7 @@ function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn- |
| 526 | aria-expanded={row.expanded} | 527 | aria-expanded={row.expanded} |
| 527 | data-scroll-anchor-ignore | 528 | data-scroll-anchor-ignore |
| 528 | onClick={() => ctx.onToggleTurnFold(row.turnId)} | 529 | onClick={() => ctx.onToggleTurnFold(row.turnId)} |
| 529 | className="flex items-center gap-1 px-1 text-xs text-muted-foreground tabular-nums transition-colors hover:text-foreground" | 530 | className="flex cursor-pointer select-none items-center gap-1 rounded-md px-1 text-xs text-muted-foreground tabular-nums transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" |
| 530 | > | 531 | > |
| 531 | <span>{row.label}</span> | 532 | <span>{row.label}</span> |
| 532 | <Icon className="size-3.5" /> | 533 | <Icon className="size-3.5" /> |
| ... | @@ -679,6 +680,8 @@ const WorkGroupSection = memo(function WorkGroupSection({ | ... | @@ -679,6 +680,8 @@ const WorkGroupSection = memo(function WorkGroupSection({ |
| 679 | }) { | 680 | }) { |
| 680 | const { workspaceRoot } = use(TimelineRowCtx); | 681 | const { workspaceRoot } = use(TimelineRowCtx); |
| 681 | const [isExpanded, setIsExpanded] = useState(false); | 682 | const [isExpanded, setIsExpanded] = useState(false); |
| | 683 | const sectionRef = useRef<HTMLElement>(null); |
| | 684 | const anchorBottomBeforeToggleRef = useRef<number | null>(null); |
| 682 | const nonEmptyEntries = useMemo( | 685 | const nonEmptyEntries = useMemo( |
| 683 | () => groupedEntries.filter((entry) => !workEntryIndicatesToolNeutralStatus(entry)), | 686 | () => groupedEntries.filter((entry) => !workEntryIndicatesToolNeutralStatus(entry)), |
| 684 | [groupedEntries], | 687 | [groupedEntries], |
| ... | @@ -690,39 +693,54 @@ const WorkGroupSection = memo(function WorkGroupSection({ | ... | @@ -690,39 +693,54 @@ const WorkGroupSection = memo(function WorkGroupSection({ |
| 690 | : nonEmptyEntries; | 693 | : nonEmptyEntries; |
| 691 | const hiddenCount = nonEmptyEntries.length - visibleEntries.length; | 694 | const hiddenCount = nonEmptyEntries.length - visibleEntries.length; |
| 692 | const onlyToolEntries = nonEmptyEntries.every((entry) => workLogEntryIsToolLike(entry)); | 695 | const onlyToolEntries = nonEmptyEntries.every((entry) => workLogEntryIsToolLike(entry)); |
| 693 | const headerTitle = onlyToolEntries | 696 | const groupLabel = onlyToolEntries |
| 694 | ? nonEmptyEntries.length === 1 | 697 | ? nonEmptyEntries.length === 1 |
| 695 | ? "1 tool call" | 698 | ? "1 tool call" |
| 696 | : `${nonEmptyEntries.length} tool calls` | 699 | : `${nonEmptyEntries.length} tool calls` |
| 697 | : "work log"; | 700 | : "work log"; |
| 698 | | 701 | |
| | 702 | useLayoutEffect(() => { |
| | 703 | const anchorBottomBeforeToggle = anchorBottomBeforeToggleRef.current; |
| | 704 | anchorBottomBeforeToggleRef.current = null; |
| | 705 | |
| | 706 | if (anchorBottomBeforeToggle === null) { |
| | 707 | return; |
| | 708 | } |
| | 709 | |
| | 710 | const section = sectionRef.current; |
| | 711 | if (!section) { |
| | 712 | return; |
| | 713 | } |
| | 714 | |
| | 715 | const delta = section.getBoundingClientRect().bottom - anchorBottomBeforeToggle; |
| | 716 | if (Math.abs(delta) < 0.5) { |
| | 717 | return; |
| | 718 | } |
| | 719 | |
| | 720 | const scroller = findNearestVerticalScroller(section); |
| | 721 | if (scroller) { |
| | 722 | scroller.scrollTop += delta; |
| | 723 | } else { |
| | 724 | window.scrollBy(0, delta); |
| | 725 | } |
| | 726 | }, [isExpanded]); |
| | 727 | |
| | 728 | const toggleExpanded = () => { |
| | 729 | anchorBottomBeforeToggleRef.current = |
| | 730 | sectionRef.current?.getBoundingClientRect().bottom ?? null; |
| | 731 | setIsExpanded((v) => !v); |
| | 732 | }; |
| | 733 | |
| 699 | if (nonEmptyEntries.length === 0) return null; | 734 | if (nonEmptyEntries.length === 0) return null; |
| 700 | | 735 | |
| 701 | return ( | 736 | return ( |
| 702 | <div className="rounded-2xl border border-input bg-background p-2 pt-3 shadow-xs/5 not-dark:bg-clip-padding dark:bg-input/32"> | 737 | <section ref={sectionRef} className="-mx-1 space-y-0.5 px-1 py-0.5" aria-label={groupLabel}> |
| 703 | <div className="mb-2 flex items-center justify-between gap-2 px-2"> | 738 | {!onlyToolEntries && ( |
| 704 | <p className="font-medium text-foreground text-xs">{headerTitle}</p> | 739 | <p className="px-0.5 pb-0.5 font-medium text-[11px] text-muted-foreground/65"> |
| 705 | {hasOverflow && ( | 740 | {groupLabel} |
| 706 | <button | 741 | </p> |
| 707 | type="button" | 742 | )} |
| 708 | className="inline-flex cursor-pointer items-center gap-1 text-muted-foreground text-xs transition-colors duration-150 hover:text-foreground/80" | 743 | <div className="space-y-px"> |
| 709 | onClick={() => setIsExpanded((v) => !v)} | | |
| 710 | > | | |
| 711 | {isExpanded ? ( | | |
| 712 | <> | | |
| 713 | Show less | | |
| 714 | <ChevronUpIcon className="size-3.5 shrink-0 opacity-80" /> | | |
| 715 | </> | | |
| 716 | ) : ( | | |
| 717 | <> | | |
| 718 | Show {hiddenCount} more | | |
| 719 | <ChevronDownIcon className="size-3.5 shrink-0 opacity-80" /> | | |
| 720 | </> | | |
| 721 | )} | | |
| 722 | </button> | | |
| 723 | )} | | |
| 724 | </div> | | |
| 725 | <div className="space-y-0.5"> | | |
| 726 | {visibleEntries.map((workEntry) => ( | 744 | {visibleEntries.map((workEntry) => ( |
| 727 | <SimpleWorkEntryRow | 745 | <SimpleWorkEntryRow |
| 728 | key={workEntry.id} | 746 | key={workEntry.id} |
| ... | @@ -731,10 +749,47 @@ const WorkGroupSection = memo(function WorkGroupSection({ | ... | @@ -731,10 +749,47 @@ const WorkGroupSection = memo(function WorkGroupSection({ |
| 731 | /> | 749 | /> |
| 732 | ))} | 750 | ))} |
| 733 | </div> | 751 | </div> |
| 734 | </div> | 752 | {hasOverflow && ( |
| | 753 | <button |
| | 754 | type="button" |
| | 755 | className="flex w-full cursor-pointer items-center gap-1.5 rounded-md px-0.5 py-0.5 text-left text-[12px] leading-5 transition-colors duration-150 hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" |
| | 756 | onClick={toggleExpanded} |
| | 757 | > |
| | 758 | <span className="flex size-5 shrink-0 items-center justify-center text-muted-foreground/65"> |
| | 759 | {isExpanded ? ( |
| | 760 | <ChevronUpIcon className="size-3.5 shrink-0 opacity-70" /> |
| | 761 | ) : ( |
| | 762 | <ChevronDownIcon className="size-3.5 shrink-0 opacity-70" /> |
| | 763 | )} |
| | 764 | </span> |
| | 765 | {isExpanded ? ( |
| | 766 | <span className="font-medium text-foreground/82">Show fewer tool calls</span> |
| | 767 | ) : ( |
| | 768 | <span className="font-medium text-foreground/82"> |
| | 769 | +{hiddenCount} previous tool {hiddenCount === 1 ? "call" : "calls"} |
| | 770 | </span> |
| | 771 | )} |
| | 772 | </button> |
| | 773 | )} |
| | 774 | </section> |
| 735 | ); | 775 | ); |
| 736 | }); | 776 | }); |
| 737 | | 777 | |
| | 778 | function findNearestVerticalScroller(element: HTMLElement): HTMLElement | null { |
| | 779 | let parent = element.parentElement; |
| | 780 | while (parent) { |
| | 781 | const { overflowY } = window.getComputedStyle(parent); |
| | 782 | if ( |
| | 783 | (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && |
| | 784 | parent.scrollHeight > parent.clientHeight |
| | 785 | ) { |
| | 786 | return parent; |
| | 787 | } |
| | 788 | parent = parent.parentElement; |
| | 789 | } |
| | 790 | return null; |
| | 791 | } |
| | 792 | |
| 738 | /** Subscribes directly to the UI state store for expand/collapse state, | 793 | /** Subscribes directly to the UI state store for expand/collapse state, |
| 739 | * so toggling re-renders only this component — not the entire list. */ | 794 | * so toggling re-renders only this component — not the entire list. */ |
| 740 | const AssistantChangedFilesSection = memo(function AssistantChangedFilesSection({ | 795 | const AssistantChangedFilesSection = memo(function AssistantChangedFilesSection({ |
| ... | @@ -1183,30 +1238,73 @@ function formatWorkingTimerNow(startIso: string): string { | ... | @@ -1183,30 +1238,73 @@ function formatWorkingTimerNow(startIso: string): string { |
| 1183 | return formatWorkingTimer(startIso, new Date().toISOString()) ?? "0s"; | 1238 | return formatWorkingTimer(startIso, new Date().toISOString()) ?? "0s"; |
| 1184 | } | 1239 | } |
| 1185 | | 1240 | |
| | 1241 | type WorkEntryIconName = |
| | 1242 | | "bot" |
| | 1243 | | "check" |
| | 1244 | | "circle-alert" |
| | 1245 | | "eye" |
| | 1246 | | "globe" |
| | 1247 | | "hammer" |
| | 1248 | | "message-circle" |
| | 1249 | | "square-pen" |
| | 1250 | | "terminal" |
| | 1251 | | "wrench" |
| | 1252 | | "x" |
| | 1253 | | "zap"; |
| | 1254 | |
| | 1255 | function WorkEntryIconSvg({ name, className }: { name: WorkEntryIconName; className: string }) { |
| | 1256 | switch (name) { |
| | 1257 | case "bot": |
| | 1258 | return <BotIcon className={className} aria-hidden />; |
| | 1259 | case "check": |
| | 1260 | return <CheckIcon className={className} aria-hidden />; |
| | 1261 | case "circle-alert": |
| | 1262 | return <CircleAlertIcon className={className} aria-hidden />; |
| | 1263 | case "eye": |
| | 1264 | return <EyeIcon className={className} aria-hidden />; |
| | 1265 | case "globe": |
| | 1266 | return <GlobeIcon className={className} aria-hidden />; |
| | 1267 | case "hammer": |
| | 1268 | return <HammerIcon className={className} aria-hidden />; |
| | 1269 | case "message-circle": |
| | 1270 | return <MessageCircleIcon className={className} aria-hidden />; |
| | 1271 | case "square-pen": |
| | 1272 | return <SquarePenIcon className={className} aria-hidden />; |
| | 1273 | case "terminal": |
| | 1274 | return <TerminalIcon className={className} aria-hidden />; |
| | 1275 | case "wrench": |
| | 1276 | return <WrenchIcon className={className} aria-hidden />; |
| | 1277 | case "x": |
| | 1278 | return <XIcon className={className} aria-hidden />; |
| | 1279 | case "zap": |
| | 1280 | return <ZapIcon className={className} aria-hidden />; |
| | 1281 | } |
| | 1282 | } |
| | 1283 | |
| 1186 | function workToneIcon(tone: TimelineWorkEntry["tone"]): { | 1284 | function workToneIcon(tone: TimelineWorkEntry["tone"]): { |
| 1187 | icon: LucideIcon; | 1285 | iconName: WorkEntryIconName; |
| 1188 | className: string; | 1286 | className: string; |
| 1189 | } { | 1287 | } { |
| 1190 | if (tone === "error") { | 1288 | if (tone === "error") { |
| 1191 | return { | 1289 | return { |
| 1192 | icon: CircleAlertIcon, | 1290 | iconName: "circle-alert", |
| 1193 | className: "text-foreground/92", | 1291 | className: "text-foreground/92", |
| 1194 | }; | 1292 | }; |
| 1195 | } | 1293 | } |
| 1196 | if (tone === "thinking") { | 1294 | if (tone === "thinking") { |
| 1197 | return { | 1295 | return { |
| 1198 | icon: BotIcon, | 1296 | iconName: "bot", |
| 1199 | className: "text-foreground/92", | 1297 | className: "text-foreground/92", |
| 1200 | }; | 1298 | }; |
| 1201 | } | 1299 | } |
| 1202 | if (tone === "info") { | 1300 | if (tone === "info") { |
| 1203 | return { | 1301 | return { |
| 1204 | icon: CheckIcon, | 1302 | iconName: "check", |
| 1205 | className: "text-muted-foreground", | 1303 | className: "text-muted-foreground", |
| 1206 | }; | 1304 | }; |
| 1207 | } | 1305 | } |
| 1208 | return { | 1306 | return { |
| 1209 | icon: ZapIcon, | 1307 | iconName: "zap", |
| 1210 | className: "text-foreground/92", | 1308 | className: "text-foreground/92", |
| 1211 | }; | 1309 | }; |
| 1212 | } | 1310 | } |
| ... | @@ -1236,52 +1334,58 @@ function workEntryRawCommand( | ... | @@ -1236,52 +1334,58 @@ function workEntryRawCommand( |
| 1236 | return rawCommand === workEntry.command.trim() ? null : rawCommand; | 1334 | return rawCommand === workEntry.command.trim() ? null : rawCommand; |
| 1237 | } | 1335 | } |
| 1238 | | 1336 | |
| 1239 | function buildToolCallExpandedBody(workEntry: TimelineWorkEntry): string | null { | 1337 | function buildToolCallExpandedBody( |
| 1240 | const blocks: string[] = []; | 1338 | workEntry: TimelineWorkEntry, |
| 1241 | if (workEntry.detail?.trim()) { | 1339 | workspaceRoot: string | undefined, |
| 1242 | blocks.push(workEntry.detail.trim()); | 1340 | ): string | null { |
| 1243 | } | | |
| 1244 | const raw = workEntryRawCommand(workEntry); | 1341 | const raw = workEntryRawCommand(workEntry); |
| 1245 | if (raw?.trim()) { | 1342 | if (raw?.trim()) { |
| 1246 | blocks.push(`Full command\n${raw.trim()}`); | 1343 | return raw.trim(); |
| 1247 | } else if (workEntry.command?.trim()) { | | |
| 1248 | blocks.push(`Command\n${workEntry.command.trim()}`); | | |
| 1249 | } | 1344 | } |
| 1250 | if (blocks.length === 0) { | 1345 | if (workEntry.command?.trim()) { |
| 1251 | return null; | 1346 | return workEntry.command.trim(); |
| | 1347 | } |
| | 1348 | if (workEntry.detail?.trim()) { |
| | 1349 | return workEntry.detail.trim(); |
| 1252 | } | 1350 | } |
| 1253 | return blocks.join("\n\n"); | 1351 | const changedFiles = workEntry.changedFiles ?? []; |
| | 1352 | if (changedFiles.length > 0) { |
| | 1353 | return changedFiles |
| | 1354 | .map((filePath) => formatWorkspaceRelativePath(filePath, workspaceRoot)) |
| | 1355 | .join("\n"); |
| | 1356 | } |
| | 1357 | return null; |
| 1254 | } | 1358 | } |
| 1255 | | 1359 | |
| 1256 | function workEntryIcon(workEntry: TimelineWorkEntry): LucideIcon { | 1360 | function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName { |
| 1257 | if ( | 1361 | if ( |
| 1258 | workEntry.sourceActivityKind === "user-input.requested" || | 1362 | workEntry.sourceActivityKind === "user-input.requested" || |
| 1259 | workEntry.sourceActivityKind === "user-input.resolved" | 1363 | workEntry.sourceActivityKind === "user-input.resolved" |
| 1260 | ) { | 1364 | ) { |
| 1261 | return MessageCircleIcon; | 1365 | return "message-circle"; |
| 1262 | } | 1366 | } |
| 1263 | if (workEntry.requestKind === "command") return TerminalIcon; | 1367 | if (workEntry.requestKind === "command") return "terminal"; |
| 1264 | if (workEntry.requestKind === "file-read") return EyeIcon; | 1368 | if (workEntry.requestKind === "file-read") return "eye"; |
| 1265 | if (workEntry.requestKind === "file-change") return SquarePenIcon; | 1369 | if (workEntry.requestKind === "file-change") return "square-pen"; |
| 1266 | | 1370 | |
| 1267 | if (workEntry.itemType === "command_execution" || workEntry.command) { | 1371 | if (workEntry.itemType === "command_execution" || workEntry.command) { |
| 1268 | return TerminalIcon; | 1372 | return "terminal"; |
| 1269 | } | 1373 | } |
| 1270 | if (workEntry.itemType === "file_change" || (workEntry.changedFiles?.length ?? 0) > 0) { | 1374 | if (workEntry.itemType === "file_change" || (workEntry.changedFiles?.length ?? 0) > 0) { |
| 1271 | return SquarePenIcon; | 1375 | return "square-pen"; |
| 1272 | } | 1376 | } |
| 1273 | if (workEntry.itemType === "web_search") return GlobeIcon; | 1377 | if (workEntry.itemType === "web_search") return "globe"; |
| 1274 | if (workEntry.itemType === "image_view") return EyeIcon; | 1378 | if (workEntry.itemType === "image_view") return "eye"; |
| 1275 | | 1379 | |
| 1276 | switch (workEntry.itemType) { | 1380 | switch (workEntry.itemType) { |
| 1277 | case "mcp_tool_call": | 1381 | case "mcp_tool_call": |
| 1278 | return WrenchIcon; | 1382 | return "wrench"; |
| 1279 | case "dynamic_tool_call": | 1383 | case "dynamic_tool_call": |
| 1280 | case "collab_agent_tool_call": | 1384 | case "collab_agent_tool_call": |
| 1281 | return HammerIcon; | 1385 | return "hammer"; |
| 1282 | } | 1386 | } |
| 1283 | | 1387 | |
| 1284 | return workToneIcon(workEntry.tone).icon; | 1388 | return workToneIcon(workEntry.tone).iconName; |
| 1285 | } | 1389 | } |
| 1286 | | 1390 | |
| 1287 | function capitalizePhrase(value: string): string { | 1391 | function capitalizePhrase(value: string): string { |
| ... | @@ -1310,7 +1414,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -1310,7 +1414,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 1310 | const [expanded, setExpanded] = useState(false); | 1414 | const [expanded, setExpanded] = useState(false); |
| 1311 | const iconConfig = workToneIcon(workEntry.tone); | 1415 | const iconConfig = workToneIcon(workEntry.tone); |
| 1312 | const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning"; | 1416 | const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning"; |
| 1313 | const EntryIcon = showWarningIndicator ? XIcon : workEntryIcon(workEntry); | 1417 | const entryIconName = showWarningIndicator ? "x" : workEntryIconName(workEntry); |
| 1314 | const heading = toolWorkEntryHeading(workEntry); | 1418 | const heading = toolWorkEntryHeading(workEntry); |
| 1315 | const rawPreview = workEntryPreview(workEntry, workspaceRoot); | 1419 | const rawPreview = workEntryPreview(workEntry, workspaceRoot); |
| 1316 | const preview = | 1420 | const preview = |
| ... | @@ -1319,11 +1423,9 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -1319,11 +1423,9 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 1319 | normalizeCompactToolLabel(heading).toLowerCase() | 1423 | normalizeCompactToolLabel(heading).toLowerCase() |
| 1320 | ? null | 1424 | ? null |
| 1321 | : rawPreview; | 1425 | : rawPreview; |
| 1322 | const rawCommand = workEntryRawCommand(workEntry); | | |
| 1323 | const displayText = preview ? `${heading} - ${preview}` : heading; | 1426 | const displayText = preview ? `${heading} - ${preview}` : heading; |
| 1324 | const expandedBody = buildToolCallExpandedBody(workEntry); | 1427 | const expandedBody = buildToolCallExpandedBody(workEntry, workspaceRoot); |
| 1325 | const canExpand = expandedBody !== null; | 1428 | const canExpand = expandedBody !== null; |
| 1326 | const hasChangedFiles = (workEntry.changedFiles?.length ?? 0) > 0; | | |
| 1327 | const showFailedIndicator = workEntryIndicatesToolFailure(workEntry); | 1429 | const showFailedIndicator = workEntryIndicatesToolFailure(workEntry); |
| 1328 | const showDestructiveRowStyle = | 1430 | const showDestructiveRowStyle = |
| 1329 | showFailedIndicator && | 1431 | showFailedIndicator && |
| ... | @@ -1335,14 +1437,14 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -1335,14 +1437,14 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 1335 | : showDestructiveRowStyle | 1437 | : showDestructiveRowStyle |
| 1336 | ? "text-destructive" | 1438 | ? "text-destructive" |
| 1337 | : workEntry.tone === "tool" || showFailedIndicator | 1439 | : workEntry.tone === "tool" || showFailedIndicator |
| 1338 | ? "text-muted-foreground" | 1440 | ? "text-muted-foreground/65" |
| 1339 | : iconConfig.className, | 1441 | : iconConfig.className, |
| 1340 | ); | 1442 | ); |
| 1341 | const headingClass = showWarningIndicator | 1443 | const headingClass = showWarningIndicator |
| 1342 | ? "font-medium text-warning" | 1444 | ? "font-medium text-warning" |
| 1343 | : showDestructiveRowStyle | 1445 | : showDestructiveRowStyle |
| 1344 | ? "font-medium text-destructive" | 1446 | ? "font-medium text-destructive" |
| 1345 | : "font-medium text-foreground"; | 1447 | : "font-medium text-foreground/82"; |
| 1346 | const turnSettled = !activity.activeTurnInProgress; | 1448 | const turnSettled = !activity.activeTurnInProgress; |
| 1347 | const showNeutralIndicator = !turnSettled && workEntryIndicatesToolNeutralStatus(workEntry); | 1449 | const showNeutralIndicator = !turnSettled && workEntryIndicatesToolNeutralStatus(workEntry); |
| 1348 | const showSuccessIndicator = | 1450 | const showSuccessIndicator = |
| ... | @@ -1352,6 +1454,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -1352,6 +1454,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 1352 | ? { | 1454 | ? { |
| 1353 | role: "button" as const, | 1455 | role: "button" as const, |
| 1354 | tabIndex: 0 as const, | 1456 | tabIndex: 0 as const, |
| | 1457 | "aria-label": displayText, |
| 1355 | onClick: () => setExpanded((v) => !v), | 1458 | onClick: () => setExpanded((v) => !v), |
| 1356 | onKeyDown: (e: KeyboardEvent<HTMLDivElement>) => { | 1459 | onKeyDown: (e: KeyboardEvent<HTMLDivElement>) => { |
| 1357 | if (e.key === "Enter" || e.key === " ") { | 1460 | if (e.key === "Enter" || e.key === " ") { |
| ... | @@ -1365,114 +1468,66 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -1365,114 +1468,66 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 1365 | return ( | 1468 | return ( |
| 1366 | <div | 1469 | <div |
| 1367 | className={cn( | 1470 | className={cn( |
| 1368 | "flex flex-col rounded-xl border border-transparent px-2 py-2 transition-colors", | 1471 | "flex flex-col rounded-md px-0.5 py-0.5 transition-colors", |
| 1369 | canExpand && | 1472 | canExpand && |
| 1370 | "cursor-pointer hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background", | 1473 | "cursor-pointer hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70", |
| 1371 | )} | 1474 | )} |
| 1372 | {...rowToggleProps} | 1475 | {...rowToggleProps} |
| 1373 | > | 1476 | > |
| 1374 | <div className="flex items-center gap-2 transition-[opacity,translate] duration-200"> | 1477 | <div className="flex select-none items-center gap-1.5 transition-[opacity,translate] duration-200"> |
| 1375 | <span className={iconWrapperClass}> | 1478 | <span className={iconWrapperClass}> |
| 1376 | <EntryIcon className="block size-3.5 shrink-0" aria-hidden /> | 1479 | <WorkEntryIconSvg |
| | 1480 | name={entryIconName} |
| | 1481 | className="block size-3.5 shrink-0 stroke-[1.8] opacity-80" |
| | 1482 | /> |
| 1377 | </span> | 1483 | </span> |
| 1378 | <div className="flex min-w-0 flex-1 items-center gap-2"> | 1484 | <div className="flex min-w-0 flex-1 items-center gap-1.5"> |
| 1379 | <div className="min-w-0 flex-1 overflow-hidden"> | 1485 | <div className="min-w-0 flex-1 overflow-hidden"> |
| 1380 | {rawCommand ? ( | 1486 | <p className="flex min-w-0 w-full items-baseline gap-1.5 text-[12px] leading-5"> |
| 1381 | <div className="max-w-full"> | 1487 | <span className={cn("min-w-0 shrink truncate", headingClass)}>{heading}</span> |
| 1382 | <Tooltip> | 1488 | {preview && ( |
| 1383 | <TooltipTrigger | 1489 | <span className="min-w-0 flex-1 truncate text-muted-foreground/55">{preview}</span> |
| 1384 | closeDelay={0} | 1490 | )} |
| 1385 | delay={75} | 1491 | </p> |
| 1386 | onClick={stopRowToggle} | | |
| 1387 | onPointerDown={stopRowToggle} | | |
| 1388 | render={ | | |
| 1389 | <p | | |
| 1390 | className="flex min-w-0 w-full items-center gap-2 text-xs leading-5" | | |
| 1391 | aria-label={displayText} | | |
| 1392 | /> | | |
| 1393 | } | | |
| 1394 | > | | |
| 1395 | <span className={cn("min-w-0 shrink truncate", headingClass)}>{heading}</span> | | |
| 1396 | {preview && ( | | |
| 1397 | <span className="min-w-0 flex-1 cursor-default truncate text-muted-foreground transition-colors hover:text-muted-foreground/90 focus-visible:text-muted-foreground/90"> | | |
| 1398 | {preview} | | |
| 1399 | </span> | | |
| 1400 | )} | | |
| 1401 | </TooltipTrigger> | | |
| 1402 | <TooltipPopup | | |
| 1403 | align="start" | | |
| 1404 | className="max-w-[min(56rem,calc(100vw-2rem))] px-0 py-0" | | |
| 1405 | side="top" | | |
| 1406 | > | | |
| 1407 | <div className="max-w-[min(56rem,calc(100vw-2rem))] overflow-x-auto px-1.5 py-1 font-mono text-[11px] leading-4 whitespace-nowrap"> | | |
| 1408 | {rawCommand} | | |
| 1409 | </div> | | |
| 1410 | </TooltipPopup> | | |
| 1411 | </Tooltip> | | |
| 1412 | </div> | | |
| 1413 | ) : ( | | |
| 1414 | <Tooltip> | | |
| 1415 | <TooltipTrigger | | |
| 1416 | className="block min-w-0 w-full text-left" | | |
| 1417 | aria-label={displayText} | | |
| 1418 | onClick={stopRowToggle} | | |
| 1419 | onPointerDown={stopRowToggle} | | |
| 1420 | > | | |
| 1421 | <p className="flex min-w-0 w-full items-center gap-2 text-[11px] leading-5"> | | |
| 1422 | <span className={cn("min-w-0 shrink truncate", headingClass)}>{heading}</span> | | |
| 1423 | {preview && ( | | |
| 1424 | <span className="min-w-0 flex-1 truncate text-muted-foreground"> | | |
| 1425 | {preview} | | |
| 1426 | </span> | | |
| 1427 | )} | | |
| 1428 | </p> | | |
| 1429 | </TooltipTrigger> | | |
| 1430 | <TooltipPopup className="max-w-[min(720px,calc(100vw-2rem))]"> | | |
| 1431 | <p className="whitespace-pre-wrap wrap-break-word text-xs leading-5"> | | |
| 1432 | {displayText} | | |
| 1433 | </p> | | |
| 1434 | </TooltipPopup> | | |
| 1435 | </Tooltip> | | |
| 1436 | )} | | |
| 1437 | </div> | 1492 | </div> |
| 1438 | <div className="flex shrink-0 items-center gap-0.5"> | 1493 | <div className="flex shrink-0 items-center gap-px text-muted-foreground/55"> |
| 1439 | <span | 1494 | <span |
| 1440 | className="flex size-5 shrink-0 items-center justify-center" | 1495 | className="flex size-4 shrink-0 items-center justify-center" |
| 1441 | aria-hidden={!canExpand} | 1496 | aria-hidden={!canExpand} |
| 1442 | > | 1497 | > |
| 1443 | {canExpand ? ( | 1498 | {canExpand ? ( |
| 1444 | <ChevronDownIcon | 1499 | <ChevronDownIcon |
| 1445 | className={cn( | 1500 | className={cn( |
| 1446 | "size-3.5 shrink-0 text-muted-foreground opacity-80 transition-transform duration-200", | 1501 | "size-3 shrink-0 opacity-70 transition-transform duration-200", |
| 1447 | expanded && "rotate-180", | 1502 | expanded && "rotate-180", |
| 1448 | )} | 1503 | )} |
| 1449 | aria-hidden | 1504 | aria-hidden |
| 1450 | /> | 1505 | /> |
| 1451 | ) : null} | 1506 | ) : null} |
| 1452 | </span> | 1507 | </span> |
| 1453 | <span className="flex size-5 shrink-0 items-center justify-center"> | 1508 | <span className="flex size-4 shrink-0 items-center justify-center"> |
| 1454 | {showFailedIndicator ? ( | 1509 | {showFailedIndicator ? ( |
| 1455 | <Tooltip> | 1510 | <Tooltip> |
| 1456 | <TooltipTrigger | 1511 | <TooltipTrigger |
| 1457 | render={ | 1512 | render={ |
| 1458 | <span | 1513 | <span |
| 1459 | className="flex size-5 items-center justify-center" | 1514 | className="flex size-4 items-center justify-center" |
| 1460 | aria-label="Tool call failed" | 1515 | aria-label="Tool call failed" |
| 1461 | /> | 1516 | /> |
| 1462 | } | 1517 | } |
| 1463 | > | 1518 | > |
| 1464 | <XIcon className="block size-3.5 shrink-0 text-destructive" aria-hidden /> | 1519 | <XIcon className="block size-3 shrink-0 text-destructive" aria-hidden /> |
| 1465 | </TooltipTrigger> | 1520 | </TooltipTrigger> |
| 1466 | <TooltipPopup>Failed</TooltipPopup> | 1521 | <TooltipPopup>Failed</TooltipPopup> |
| 1467 | </Tooltip> | 1522 | </Tooltip> |
| 1468 | ) : showSuccessIndicator ? ( | 1523 | ) : showSuccessIndicator ? ( |
| 1469 | <Tooltip> | 1524 | <Tooltip> |
| 1470 | <TooltipTrigger | 1525 | <TooltipTrigger |
| 1471 | render={<span className="flex size-5 items-center justify-center" />} | 1526 | render={<span className="flex size-4 items-center justify-center" />} |
| 1472 | > | 1527 | > |
| 1473 | <span className="inline-flex size-5 items-center justify-center text-muted-foreground"> | 1528 | <span className="inline-flex size-4 items-center justify-center"> |
| 1474 | <CheckIcon | 1529 | <CheckIcon |
| 1475 | className="block size-3.5 shrink-0 stroke-current" | 1530 | className="block size-3 shrink-0 stroke-current" |
| 1476 | stroke="currentColor" | 1531 | stroke="currentColor" |
| 1477 | aria-hidden | 1532 | aria-hidden |
| 1478 | /> | 1533 | /> |
| ... | @@ -1483,12 +1538,9 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -1483,12 +1538,9 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 1483 | ) : showNeutralIndicator ? ( | 1538 | ) : showNeutralIndicator ? ( |
| 1484 | <Tooltip> | 1539 | <Tooltip> |
| 1485 | <TooltipTrigger | 1540 | <TooltipTrigger |
| 1486 | render={<span className="flex size-5 items-center justify-center" />} | 1541 | render={<span className="flex size-4 items-center justify-center" />} |
| 1487 | > | 1542 | > |
| 1488 | <MinusIcon | 1543 | <MinusIcon className="block size-3 shrink-0 opacity-70" aria-hidden /> |
| 1489 | className="block size-3.5 shrink-0 text-muted-foreground opacity-80" | | |
| 1490 | aria-hidden | | |
| 1491 | /> | | |
| 1492 | </TooltipTrigger> | 1544 | </TooltipTrigger> |
| 1493 | <TooltipPopup>Empty</TooltipPopup> | 1545 | <TooltipPopup>Empty</TooltipPopup> |
| 1494 | </Tooltip> | 1546 | </Tooltip> |
| ... | @@ -1499,48 +1551,15 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ... | @@ -1499,48 +1551,15 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { |
| 1499 | </div> | 1551 | </div> |
| 1500 | {expanded && canExpand && expandedBody ? ( | 1552 | {expanded && canExpand && expandedBody ? ( |
| 1501 | <div | 1553 | <div |
| 1502 | className="mt-2 ms-7 border-s border-border/45 ps-3 pt-0.5" | 1554 | className="mt-1 ms-7 cursor-default border-s border-border/45 ps-3 pt-0.5" |
| 1503 | onClick={stopRowToggle} | 1555 | onClick={stopRowToggle} |
| 1504 | onPointerDown={stopRowToggle} | 1556 | onPointerDown={stopRowToggle} |
| 1505 | > | 1557 | > |
| 1506 | <pre className="max-h-64 overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed text-muted-foreground"> | 1558 | <pre className="max-h-64 cursor-text overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed text-muted-foreground select-text"> |
| 1507 | {expandedBody} | 1559 | {expandedBody} |
| 1508 | </pre> | 1560 | </pre> |
| 1509 | </div> | 1561 | </div> |
| 1510 | ) : null} | 1562 | ) : null} |
| 1511 | {hasChangedFiles && ( | | |
| 1512 | <div | | |
| 1513 | className="mt-1 flex flex-wrap gap-1" | | |
| 1514 | onClick={stopRowToggle} | | |
| 1515 | onPointerDown={stopRowToggle} | | |
| 1516 | > | | |
| 1517 | {workEntry.changedFiles?.slice(0, 4).map((filePath) => { | | |
| 1518 | const displayPath = formatWorkspaceRelativePath(filePath, workspaceRoot); | | |
| 1519 | return ( | | |
| 1520 | <Tooltip key={`${workEntry.id}:${filePath}`}> | | |
| 1521 | <TooltipTrigger | | |
| 1522 | render={ | | |
| 1523 | <span | | |
| 1524 | className="rounded-md border border-border/55 bg-background/75 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground/75" | | |
| 1525 | aria-label={displayPath} | | |
| 1526 | /> | | |
| 1527 | } | | |
| 1528 | > | | |
| 1529 | {displayPath} | | |
| 1530 | </TooltipTrigger> | | |
| 1531 | <TooltipPopup side="top" className="max-w-[min(40rem,calc(100vw-2rem))]"> | | |
| 1532 | <span className="font-mono text-[11px] whitespace-nowrap">{displayPath}</span> | | |
| 1533 | </TooltipPopup> | | |
| 1534 | </Tooltip> | | |
| 1535 | ); | | |
| 1536 | })} | | |
| 1537 | {(workEntry.changedFiles?.length ?? 0) > 4 && ( | | |
| 1538 | <span className="px-1 text-[10px] text-muted-foreground/55"> | | |
| 1539 | +{(workEntry.changedFiles?.length ?? 0) - 4} | | |
| 1540 | </span> | | |
| 1541 | )} | | |
| 1542 | </div> | | |
| 1543 | )} | | |
| 1544 | </div> | 1563 | </div> |
| 1545 | ); | 1564 | ); |
| 1546 | }); | 1565 | }); |