diff --git a/packages/app/src/agent-stream/disclosure.browser.test.tsx b/packages/app/src/agent-stream/disclosure.browser.test.tsx
index 74c61c4988..d953130a1c 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,63 @@ 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({ hasFile }: { hasFile: boolean }) {
+ const [expanded, setExpanded] = React.useState(false);
+ const toggle = React.useCallback(() => setExpanded(!expanded), [expanded]);
+ const openFile = React.useCallback(() => {
+ opened += 1;
+ }, []);
+ return (
+
+ );
+ }
+ container.style.width = "300px";
+ container.style.marginLeft = "20px";
+ act(() => root.render());
+ const header = page.getByRole("button", { name: "Read src/index.ts", exact: true });
+ const headerElement = view().getByRole("button", { name: /^Read src\/index\.ts$/ });
+ expect(headerElement.getBoundingClientRect().width).toBeGreaterThan(260);
+ await header.click({ position: { x: 220, y: 10 } });
+ await expect.element(page.getByText("File contents")).toBeVisible();
+ await header.click({ position: { x: 2, y: 2 } });
+ await expect.element(page.getByText("File contents")).not.toBeInTheDocument();
+ await header.click({ position: { x: 2, y: 2 } });
+ await expect.element(page.getByText("File contents")).toBeVisible();
+ await header.click({ position: { x: 220, y: 10 } });
+ await expect.element(page.getByText("File contents")).not.toBeInTheDocument();
+ 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 header.click({ position: { x: 220, y: 10 } });
+
+ await expect.element(page.getByText("File contents")).toBeVisible();
+ 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..087555e8a0 100644
--- a/packages/app/src/components/message.tsx
+++ b/packages/app/src/components/message.tsx
@@ -922,13 +922,19 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.lg,
borderWidth: theme.borderWidth[1],
borderColor: "transparent",
- paddingHorizontal: theme.spacing[2],
- paddingVertical: theme.spacing[1],
overflow: "hidden",
},
pressablePressed: {
opacity: 0.9,
},
+ disclosureButton: {
+ flex: 1,
+ minWidth: 0,
+ paddingHorizontal: theme.spacing[2],
+ paddingVertical: theme.spacing[1],
+ flexDirection: "row",
+ alignItems: "center",
+ },
headerRow: {
flexDirection: "row",
alignItems: "center",
@@ -984,15 +990,12 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
flexShrink: 0,
},
openFileButton: {
+ marginRight: theme.spacing[2],
marginLeft: theme.spacing[1],
padding: theme.spacing[1],
borderRadius: theme.borderRadius.md,
flexShrink: 0,
},
- openFileButtonPlaceholderIcon: {
- width: 14,
- height: 14,
- },
detailWrapper: {
borderBottomLeftRadius: theme.borderRadius.lg,
borderBottomRightRadius: theme.borderRadius.lg,
@@ -1598,7 +1601,6 @@ interface ExpandableBadgeWebShimmerOverlayProps {
secondaryLabel?: string;
shimmerLabelTextStyle: StyleProp;
shimmerSecondaryTextStyle: StyleProp;
- showOpenFileButton: boolean;
}
function ExpandableBadgeWebShimmerOverlay({
@@ -1606,7 +1608,6 @@ function ExpandableBadgeWebShimmerOverlay({
secondaryLabel,
shimmerLabelTextStyle,
shimmerSecondaryTextStyle,
- showOpenFileButton,
}: ExpandableBadgeWebShimmerOverlayProps) {
return (
@@ -1618,14 +1619,7 @@ function ExpandableBadgeWebShimmerOverlay({
{secondaryLabel}
) : null}
- {showOpenFileButton ? (
-
-
-
- ) : null}
- {!secondaryLabel && !showOpenFileButton ? (
-
- ) : null}
+ {!secondaryLabel ? : null}
);
}
@@ -1649,11 +1643,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 +1664,7 @@ function ExpandableBadgeLabelRow({
onLabelRowLayout,
onLabelLayout,
onSecondaryLayout,
- showOpenFileButton,
- isOpenFileHovered,
- onOpenFilePress,
- onOpenFileHoverIn,
- onOpenFileHoverOut,
}: ExpandableBadgeLabelRowProps) {
- const { t } = useTranslation();
return (
- {showOpenFileButton ? (
-
-
-
- ) : null}
{isWebShimmer ? (
) : null}
{isNativeShimmer ? (
@@ -1928,6 +1893,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 +2156,13 @@ export const ExpandableBadge = memo(function ExpandableBadge({
onPointerEnter={isWeb ? handleHoverIn : undefined}
onPointerLeave={isWeb ? handleHoverOut : undefined}
>
-
-
+
+
{iconSlotNode}
-
-
+
+ {onOpenFile && isHovered ? (
+
+
+
+ ) : null}
+
{detailContent ? (