diff --git a/packages/app/src/agent-stream/disclosure.browser.test.tsx b/packages/app/src/agent-stream/disclosure.browser.test.tsx
index 74c61c4988..b46f10b2b2 100644
--- a/packages/app/src/agent-stream/disclosure.browser.test.tsx
+++ b/packages/app/src/agent-stream/disclosure.browser.test.tsx
@@ -4,6 +4,8 @@ import { within } from "@testing-library/dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { i18n as testI18n } from "@/i18n/i18next";
+import { ExpandableBadge } from "@/components/message";
+import { page, userEvent } from "vitest/browser";
import { PlanCard } from "@/components/plan-card";
import { MessagePreview } from "@/components/user-message";
import { DisclosureRow } from "./disclosure";
@@ -145,3 +147,49 @@ describe("timeline disclosures hold their row", () => {
expect(isExpanded()).toBe(true);
});
});
+
+function renderFileDetails() {
+ return
File contents
;
+}
+
+describe("tool badge file action", () => {
+ it("keeps file opening separate from the accessible disclosure button", async () => {
+ let opened = 0;
+ function Badge() {
+ const [expanded, setExpanded] = React.useState(false);
+ const toggle = React.useCallback(() => setExpanded(!expanded), [expanded]);
+ const openFile = React.useCallback(() => {
+ opened += 1;
+ }, []);
+ return (
+
+ );
+ }
+ act(() => root.render());
+ await page.getByRole("button", { name: "Read src/index.ts", exact: true }).hover();
+ const file = view().getByRole("button", { name: /^Open file$/ });
+ expect(file.parentElement?.closest("button")).toBeNull();
+ await page.getByRole("button", { name: "Open file", exact: true }).click();
+ expect(opened).toBe(1);
+ const disclosure = view().getByRole("button", { name: /^Read src\/index\.ts$/ });
+ expect(view().queryByText("File contents")).toBeNull();
+ await page.getByRole("button", { name: "Read src/index.ts", exact: true }).click();
+
+ expect(view().getByText("File contents")).toBeTruthy();
+ expect(opened).toBe(1);
+ act(() => file.focus());
+ await userEvent.keyboard("{Enter}");
+ expect(opened).toBe(2);
+ expect(view().getByText("File contents")).toBeTruthy();
+ act(() => disclosure.focus());
+ await userEvent.keyboard("{Enter}");
+ expect(view().queryByText("File contents")).toBeNull();
+ });
+});
diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx
index 2bc96f6b97..8f3f16cf51 100644
--- a/packages/app/src/components/message.tsx
+++ b/packages/app/src/components/message.tsx
@@ -929,12 +929,17 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
pressablePressed: {
opacity: 0.9,
},
+ disclosureButton: {
+ flexShrink: 1,
+ flexDirection: "row",
+ alignItems: "center",
+ },
headerRow: {
flexDirection: "row",
alignItems: "center",
},
labelRow: {
- flex: 1,
+ flexShrink: 1,
flexDirection: "row",
alignItems: "center",
overflow: "hidden",
@@ -989,10 +994,6 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.md,
flexShrink: 0,
},
- openFileButtonPlaceholderIcon: {
- width: 14,
- height: 14,
- },
detailWrapper: {
borderBottomLeftRadius: theme.borderRadius.lg,
borderBottomRightRadius: theme.borderRadius.lg,
@@ -1598,7 +1599,6 @@ interface ExpandableBadgeWebShimmerOverlayProps {
secondaryLabel?: string;
shimmerLabelTextStyle: StyleProp;
shimmerSecondaryTextStyle: StyleProp;
- showOpenFileButton: boolean;
}
function ExpandableBadgeWebShimmerOverlay({
@@ -1606,7 +1606,6 @@ function ExpandableBadgeWebShimmerOverlay({
secondaryLabel,
shimmerLabelTextStyle,
shimmerSecondaryTextStyle,
- showOpenFileButton,
}: ExpandableBadgeWebShimmerOverlayProps) {
return (
@@ -1618,14 +1617,7 @@ function ExpandableBadgeWebShimmerOverlay({
{secondaryLabel}
) : null}
- {showOpenFileButton ? (
-
-
-
- ) : null}
- {!secondaryLabel && !showOpenFileButton ? (
-
- ) : null}
+ {!secondaryLabel ? : null}
);
}
@@ -1649,11 +1641,6 @@ interface ExpandableBadgeLabelRowProps {
onLabelRowLayout: (event: LayoutChangeEvent) => void;
onLabelLayout: (event: LayoutChangeEvent) => void;
onSecondaryLayout: (event: LayoutChangeEvent) => void;
- showOpenFileButton: boolean;
- isOpenFileHovered: boolean;
- onOpenFilePress: (event: GestureResponderEvent) => void;
- onOpenFileHoverIn: () => void;
- onOpenFileHoverOut: () => void;
}
function ExpandableBadgeLabelRow({
@@ -1675,13 +1662,7 @@ function ExpandableBadgeLabelRow({
onLabelRowLayout,
onLabelLayout,
onSecondaryLayout,
- showOpenFileButton,
- isOpenFileHovered,
- onOpenFilePress,
- onOpenFileHoverIn,
- onOpenFileHoverOut,
}: ExpandableBadgeLabelRowProps) {
- const { t } = useTranslation();
return (
- {showOpenFileButton ? (
-
-
-
- ) : null}
{isWebShimmer ? (
) : null}
{isNativeShimmer ? (
@@ -1928,6 +1891,7 @@ export const ExpandableBadge = memo(function ExpandableBadge({
borderlessWhenExpanded = false,
testID,
}: ExpandableBadgeProps) {
+ const { t } = useTranslation();
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
const [isHovered, setIsHovered] = useState(false);
const [isOpenFileHovered, setIsOpenFileHovered] = useState(false);
@@ -2190,13 +2154,13 @@ export const ExpandableBadge = memo(function ExpandableBadge({
onPointerEnter={isWeb ? handleHoverIn : undefined}
onPointerLeave={isWeb ? handleHoverOut : undefined}
>
-
-
+
+
{iconSlotNode}
-
-
+
+ {onOpenFile && isHovered ? (
+
+
+
+ ) : null}
+
{detailContent ? (