mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-15 12:54:58 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ea11b814c | ||
|
|
3373a73d38 |
@@ -401,3 +401,123 @@ test("forms a closed group when the lone tool call was left closed", async ({ pa
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
const longGroupedDetail = {
|
||||
type: "unknown",
|
||||
input: Array.from({ length: 80 }, (_, i) => `Long input ${i}`),
|
||||
output: Array.from({ length: 80 }, (_, i) => `Long output ${i}`),
|
||||
};
|
||||
|
||||
async function provideLongToolDetails(page: Page) {
|
||||
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
|
||||
const server = ws.connectToServer();
|
||||
ws.onMessage((message) => server.send(message));
|
||||
server.onMessage((message) => {
|
||||
const raw = typeof message === "string" ? message : message.toString("utf8");
|
||||
const envelope = JSON.parse(raw);
|
||||
const item = envelope.message?.payload?.event?.item;
|
||||
if (item?.type === "tool_call" && item.status === "completed") {
|
||||
item.detail = longGroupedDetail;
|
||||
}
|
||||
ws.send(JSON.stringify(envelope));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleAndMeasureTool(badge: Locator) {
|
||||
await badge.scrollIntoViewIfNeeded();
|
||||
await badge.evaluate((node) => {
|
||||
const group = node.closest('[data-testid="tool-call-group"]');
|
||||
const frames: { top: number; height: number; groupHeight: number }[] = [];
|
||||
(node as HTMLElement & { disclosureFrames: typeof frames }).disclosureFrames = frames;
|
||||
node.querySelector("button")!.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
function measure() {
|
||||
const bounds = node.getBoundingClientRect();
|
||||
frames.push({
|
||||
top: bounds.top,
|
||||
height: bounds.height,
|
||||
groupHeight: group?.getBoundingClientRect().height ?? 433,
|
||||
});
|
||||
if (frames.length < 20) requestAnimationFrame(measure);
|
||||
}
|
||||
requestAnimationFrame(measure);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
await openToolDetails(badge);
|
||||
await expect
|
||||
.poll(() =>
|
||||
badge.evaluate(
|
||||
(node) => (node as HTMLElement & { disclosureFrames: unknown[] }).disclosureFrames.length,
|
||||
),
|
||||
)
|
||||
.toBe(20);
|
||||
return badge.evaluate(
|
||||
(node) =>
|
||||
(
|
||||
node as HTMLElement & {
|
||||
disclosureFrames: { top: number; height: number; groupHeight: number }[];
|
||||
}
|
||||
).disclosureFrames,
|
||||
);
|
||||
}
|
||||
|
||||
for (const display of ["collapseToolCalls", "collapseTurns"] as const) {
|
||||
test(`keeps multi-section tool details within the group (${display})`, async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
const agent = await createOverviewAgent(page, "Bounded grouped tool details", display);
|
||||
try {
|
||||
await provideLongToolDetails(page);
|
||||
await openOverviewAgent(page, agent);
|
||||
await agent.client.sendAgentMessage(agent.agentId, "Exercise bounded grouped details.");
|
||||
await agent.client.waitForAgentUpsert(
|
||||
agent.agentId,
|
||||
(snapshot) => snapshot.status === "idle",
|
||||
30_000,
|
||||
);
|
||||
await scrollChatAwayFromBottom(page, { deltaY: -10_000, minDistanceFromBottom: 1 });
|
||||
const group = page.getByTestId("tool-call-group").first();
|
||||
if (display === "collapseTurns") {
|
||||
if ((await page.getByTestId("tool-call-badge").count()) === 0) {
|
||||
await openToolDetails(page.getByTestId("turn-summary").first());
|
||||
}
|
||||
} else {
|
||||
await expect(group).toBeVisible();
|
||||
if ((await group.getByTestId("tool-call-badge").count()) === 0)
|
||||
await openToolDetails(group);
|
||||
}
|
||||
const scope = display === "collapseTurns" ? page : group;
|
||||
const badge = scope.getByTestId("tool-call-badge").filter({ hasText: "Read" }).first();
|
||||
await expect(badge).toBeVisible();
|
||||
const expandedFrames = await toggleAndMeasureTool(badge);
|
||||
for (const frame of expandedFrames)
|
||||
expect(frame.height).toBeLessThanOrEqual(frame.groupHeight);
|
||||
expect(
|
||||
Math.max(...expandedFrames.map((frame) => frame.top)) -
|
||||
Math.min(...expandedFrames.map((frame) => frame.top)),
|
||||
).toBeLessThanOrEqual(1);
|
||||
await expect(badge).toContainText("Long output");
|
||||
await page.screenshot({
|
||||
path: `../../.artifacts/group-${process.env.HEIGHT_EVIDENCE ?? "after"}.png`,
|
||||
});
|
||||
const childBox = await badge.boundingBox();
|
||||
if (display === "collapseToolCalls") {
|
||||
const groupBox = await group.boundingBox();
|
||||
expect(childBox!.height).toBeLessThanOrEqual(groupBox!.height);
|
||||
} else {
|
||||
expect(childBox!.height).toBeLessThanOrEqual(433);
|
||||
}
|
||||
const collapsedFrames = await toggleAndMeasureTool(badge);
|
||||
expect(
|
||||
Math.max(...collapsedFrames.map((frame) => frame.top)) -
|
||||
Math.min(...collapsedFrames.map((frame) => frame.top)),
|
||||
).toBeLessThanOrEqual(1);
|
||||
await expect(badge).not.toContainText("Long output");
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { ToolCallDetailsContent } from "@/components/tool-call-details";
|
||||
import { ExpandableBadge } from "@/components/message";
|
||||
import { page, userEvent } from "vitest/browser";
|
||||
import { PlanCard } from "@/components/plan-card";
|
||||
@@ -202,3 +203,28 @@ describe("tool badge file action", () => {
|
||||
expect(view().queryByText("File contents")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const longDetail = {
|
||||
type: "unknown" as const,
|
||||
input: { text: "input\n".repeat(80) },
|
||||
output: { text: "output\n".repeat(80) },
|
||||
};
|
||||
|
||||
describe("inline tool detail height", () => {
|
||||
it("caps the complete detail, including input, output and error, to its allowance", () => {
|
||||
act(() =>
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToolCallDetailsContent
|
||||
detail={longDetail}
|
||||
errorText={"error\n".repeat(80)}
|
||||
maxHeight={200}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
);
|
||||
expect(container.getBoundingClientRect().height).toBeLessThanOrEqual(200);
|
||||
expect(container.textContent).toContain("output");
|
||||
expect(container.textContent).toContain("error");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,7 +49,6 @@ interface DetailStyles {
|
||||
jsonScrollErrorCombined: StyleProp<ViewStyle>;
|
||||
fullBleedContainerStyle: StyleProp<ViewStyle>;
|
||||
loadingContainerStyle: StyleProp<ViewStyle>;
|
||||
resolvedMaxHeight: number | undefined;
|
||||
shouldFill: boolean;
|
||||
isFullBleed: boolean;
|
||||
}
|
||||
@@ -69,7 +68,6 @@ function resolveShouldFill(
|
||||
|
||||
function useDetailStyles(
|
||||
detail: ToolCallDetail | undefined,
|
||||
resolvedMaxHeight: number | undefined,
|
||||
fillAvailableHeight: boolean,
|
||||
): DetailStyles {
|
||||
const isFullBleed = resolveIsFullBleed(detail);
|
||||
@@ -85,28 +83,14 @@ function useDetailStyles(
|
||||
[codeBlockStyle, shouldFill],
|
||||
);
|
||||
const codeVerticalScrollStyle = useMemo(
|
||||
() => [
|
||||
styles.codeVerticalScroll,
|
||||
resolvedMaxHeight !== undefined && inlineUnistylesStyle({ maxHeight: resolvedMaxHeight }),
|
||||
shouldFill && styles.fillHeight,
|
||||
],
|
||||
[resolvedMaxHeight, shouldFill],
|
||||
() => [styles.codeVerticalScroll, shouldFill && styles.fillHeight],
|
||||
[shouldFill],
|
||||
);
|
||||
const scrollAreaFillStyle = useMemo(
|
||||
() => [
|
||||
styles.scrollArea,
|
||||
resolvedMaxHeight !== undefined && inlineUnistylesStyle({ maxHeight: resolvedMaxHeight }),
|
||||
shouldFill && styles.fillHeight,
|
||||
],
|
||||
[resolvedMaxHeight, shouldFill],
|
||||
);
|
||||
const scrollAreaStyle = useMemo(
|
||||
() => [
|
||||
styles.scrollArea,
|
||||
resolvedMaxHeight !== undefined && inlineUnistylesStyle({ maxHeight: resolvedMaxHeight }),
|
||||
],
|
||||
[resolvedMaxHeight],
|
||||
() => [styles.scrollArea, shouldFill && styles.fillHeight],
|
||||
[shouldFill],
|
||||
);
|
||||
const scrollAreaStyle = useMemo(() => [styles.scrollArea], []);
|
||||
const jsonScrollCombined = styles.jsonScroll;
|
||||
const jsonScrollErrorCombined = [styles.jsonScroll, styles.jsonScrollError];
|
||||
const fullBleedContainerStyle = useMemo(
|
||||
@@ -131,7 +115,6 @@ function useDetailStyles(
|
||||
jsonScrollErrorCombined,
|
||||
fullBleedContainerStyle,
|
||||
loadingContainerStyle,
|
||||
resolvedMaxHeight,
|
||||
shouldFill,
|
||||
isFullBleed,
|
||||
};
|
||||
@@ -196,11 +179,7 @@ function EditDetailSection({ diffLines, ds }: EditDetailProps) {
|
||||
<View style={ds.sectionFillStyle}>
|
||||
{diffLines ? (
|
||||
<View style={ds.codeBlockFillStyle}>
|
||||
<DiffViewer
|
||||
diffLines={diffLines}
|
||||
maxHeight={ds.resolvedMaxHeight}
|
||||
fillAvailableHeight={ds.shouldFill}
|
||||
/>
|
||||
<DiffViewer diffLines={diffLines} fillAvailableHeight={ds.shouldFill} />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -548,7 +527,8 @@ export function ToolCallDetailsContent({
|
||||
}: ToolCallDetailsContentProps) {
|
||||
const { t } = useTranslation();
|
||||
const resolvedMaxHeight = fillAvailableHeight ? undefined : (maxHeight ?? 300);
|
||||
const ds = useDetailStyles(detail, resolvedMaxHeight, fillAvailableHeight);
|
||||
// The allowance belongs to the whole disclosure, rather than each section.
|
||||
const ds = useDetailStyles(detail, fillAvailableHeight);
|
||||
const diffLines = useDiffLines(detail);
|
||||
|
||||
const sections: ReactNode[] = buildDetailSections(toolName, detail, diffLines, ds, t);
|
||||
@@ -564,6 +544,17 @@ export function ToolCallDetailsContent({
|
||||
return <Text style={styles.emptyStateText}>{t("toolCallDetails.empty")}</Text>;
|
||||
}
|
||||
|
||||
if (resolvedMaxHeight !== undefined) {
|
||||
return (
|
||||
<ScrollView
|
||||
style={inlineUnistylesStyle({ maxHeight: resolvedMaxHeight })}
|
||||
contentContainerStyle={ds.fullBleedContainerStyle}
|
||||
nestedScrollEnabled
|
||||
>
|
||||
{sections}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
return <View style={ds.fullBleedContainerStyle}>{sections}</View>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user