authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-11 18:18:27-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-11 18:18:27-07:00
log649f4328eacff110e3a6c11457be3d43be0d1e60
tree3bb7b1ff95b9272516df5438f4be07c17c470df3
parent7db034905b49e6c3448057e484a50e14ebb64d80
signature Signed by PGP key B5690EEEBB952194

[codex] Refine inline tool call timeline UI (#3052)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 202 insertions(+), 197 deletions(-)

apps/desktop/src/backend/tailscaleEndpointProvider.ts+1-1
...@@ -121,7 +121,7 @@ export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAd...@@ -121,7 +121,7 @@ export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAd
121 input.readMagicDnsName ??121 input.readMagicDnsName ??
122 readTailscaleStatus.pipe(122 readTailscaleStatus.pipe(
123 Effect.map((status) => status.magicDnsName),123 Effect.map((status) => status.magicDnsName),
124 Effect.catch(() => Effect.succeed<string | null>(null)),124 Effect.orElseSucceed(() => null),
125 );125 );
126 const dnsName =126 const dnsName =
127 input.statusJson === undefined127 input.statusJson === undefined
apps/mobile/src/app/settings/index.tsx+2-7
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { useAuth, useUser, useUserProfileModal } from "@clerk/expo";1import { useAuth, useUser } from "@clerk/expo";
2import * as Notifications from "expo-notifications";2import * as Notifications from "expo-notifications";
3import { Link, Stack, useRouter } from "expo-router";3import { Link, Stack, useRouter } from "expo-router";
4import { SymbolView } from "expo-symbols";4import { SymbolView } from "expo-symbols";
...@@ -68,7 +68,6 @@ function ConfiguredSettingsRouteScreen() {...@@ -68,7 +68,6 @@ function ConfiguredSettingsRouteScreen() {
68 const { push } = useRouter();68 const { push } = useRouter();
69 const { getToken, isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false });69 const { getToken, isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false });
70 const { user } = useUser();70 const { user } = useUser();
71 const { isAvailable: isUserProfileModalAvailable, presentUserProfile } = useUserProfileModal();
72 const { savedConnectionsById } = useRemoteEnvironmentState();71 const { savedConnectionsById } = useRemoteEnvironmentState();
73 const [notificationStatus, setNotificationStatus] = useState<NotificationStatus>("checking");72 const [notificationStatus, setNotificationStatus] = useState<NotificationStatus>("checking");
74 const [liveActivityStatus, setLiveActivityStatus] = useState<LiveActivityStatus>("checking");73 const [liveActivityStatus, setLiveActivityStatus] = useState<LiveActivityStatus>("checking");
...@@ -266,15 +265,11 @@ function ConfiguredSettingsRouteScreen() {...@@ -266,15 +265,11 @@ function ConfiguredSettingsRouteScreen() {
266 push("/settings/waitlist");265 push("/settings/waitlist");
267 return;266 return;
268 }267 }
269 if (isUserProfileModalAvailable) {
270 void presentUserProfile();
271 return;
272 }
273 Alert.alert(268 Alert.alert(
274 "T3 Cloud unavailable",269 "T3 Cloud unavailable",
275 "Native T3 Cloud account management is not available in this build.",270 "Native T3 Cloud account management is not available in this build.",
276 );271 );
277 }, [isLoaded, isSignedIn, isUserProfileModalAvailable, presentUserProfile, push]);272 }, [isLoaded, isSignedIn, push]);
278273
279 return (274 return (
280 <View collapsable={false} className="flex-1 bg-sheet">275 <View collapsable={false} className="flex-1 bg-sheet">
apps/web/src/components/chat/MessagesTimeline.browser.tsx+6-15
...@@ -149,7 +149,7 @@ describe("MessagesTimeline", () => {...@@ -149,7 +149,7 @@ describe("MessagesTimeline", () => {
149 }149 }
150 });150 });
151151
152 it("uses accessible tooltips instead of native titles for work entry details", async () => {152 it("uses accessible expansion instead of native titles or preview tooltips for work entry details", async () => {
153 const screen = await render(153 const screen = await render(
154 <MessagesTimeline154 <MessagesTimeline
155 {...buildProps()}155 {...buildProps()}
...@@ -181,21 +181,12 @@ describe("MessagesTimeline", () => {...@@ -181,21 +181,12 @@ describe("MessagesTimeline", () => {
181 "Command - git diff -- apps/web/src/components/ChatMarkdown.tsx",181 "Command - git diff -- apps/web/src/components/ChatMarkdown.tsx",
182 );182 );
183 await commandTrigger.hover();183 await commandTrigger.hover();
184 await vi.waitFor(() => {184 expect(document.querySelector('[data-slot="tooltip-popup"]')).toBeNull();
185 const tooltip = document.querySelector<HTMLElement>('[data-slot="tooltip-popup"]');
186 expect(tooltip?.textContent).toContain(
187 "git diff -- apps/web/src/components/ChatMarkdown.tsx --stat",
188 );
189 });
190185
191 const fileTrigger = page.getByLabelText("repo/apps/web/src/components/ChatMarkdown.tsx", {186 await commandTrigger.click();
192 exact: true,187 await expect
193 });188 .element(page.getByText("git diff -- apps/web/src/components/ChatMarkdown.tsx --stat"))
194 await fileTrigger.hover();189 .toBeVisible();
195 await vi.waitFor(() => {
196 const tooltip = document.querySelector<HTMLElement>('[data-slot="tooltip-popup"]');
197 expect(tooltip?.textContent).toContain("apps/web/src/components/ChatMarkdown.tsx");
198 });
199 } finally {190 } finally {
200 await screen.unmount();191 await screen.unmount();
201 }192 }
apps/web/src/components/chat/MessagesTimeline.tsx+193-174
...@@ -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, so397 // 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.showAssistantMeta399 (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-ignore528 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 = onlyToolEntries696 const groupLabel = onlyToolEntries
694 ? nonEmptyEntries.length === 1697 ? 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";
698701
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;
700735
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 <button741 </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 <SimpleWorkEntryRow745 <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});
737777
778function 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. */
740const AssistantChangedFilesSection = memo(function AssistantChangedFilesSection({795const 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}
11851240
1241type 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
1255function 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
1186function workToneIcon(tone: TimelineWorkEntry["tone"]): {1284function 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}
12381336
1239function buildToolCallExpandedBody(workEntry: TimelineWorkEntry): string | null {1337function 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}
12551359
1256function workEntryIcon(workEntry: TimelineWorkEntry): LucideIcon {1360function 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";
12661370
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";
12751379
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 }
12831387
1284 return workToneIcon(workEntry.tone).icon;1388 return workToneIcon(workEntry.tone).iconName;
1285}1389}
12861390
1287function capitalizePhrase(value: string): string {1391function 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 ? null1424 ? 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 : showDestructiveRowStyle1437 : showDestructiveRowStyle
1336 ? "text-destructive"1438 ? "text-destructive"
1337 : workEntry.tone === "tool" || showFailedIndicator1439 : workEntry.tone === "tool" || showFailedIndicator
1338 ? "text-muted-foreground"1440 ? "text-muted-foreground/65"
1339 : iconConfig.className,1441 : iconConfig.className,
1340 );1442 );
1341 const headingClass = showWarningIndicator1443 const headingClass = showWarningIndicator
1342 ? "font-medium text-warning"1444 ? "font-medium text-warning"
1343 : showDestructiveRowStyle1445 : 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 <div1469 <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 <TooltipTrigger1489 <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 <span1494 <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 <ChevronDownIcon1499 <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-hidden1504 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 <TooltipTrigger1511 <TooltipTrigger
1457 render={1512 render={
1458 <span1513 <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 <TooltipTrigger1525 <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 <CheckIcon1529 <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-hidden1532 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 <TooltipTrigger1540 <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 <MinusIcon1543 <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 <div1553 <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});