Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b275206a9 | ||
|
|
cdf97afbc4 | ||
|
|
ca10d8f384 |
@@ -0,0 +1,69 @@
|
|||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
|
import { AgentActivityTimeline } from "./AgentActivityTimeline";
|
||||||
|
|
||||||
|
describe("AgentActivityTimeline", () => {
|
||||||
|
it("shows only the first lines of a long tool command until expanded", () => {
|
||||||
|
const target = [
|
||||||
|
"cd /app/data/conversation-workspaces/conversation-example",
|
||||||
|
"python3 - <<'EOF'",
|
||||||
|
"import json, glob",
|
||||||
|
"print('fourth line')",
|
||||||
|
"EOF",
|
||||||
|
].join("\n");
|
||||||
|
render(
|
||||||
|
<AgentActivityTimeline
|
||||||
|
activities={[
|
||||||
|
{
|
||||||
|
id: "analysis",
|
||||||
|
title: "处理分析数据",
|
||||||
|
reason: "正在核查结果。",
|
||||||
|
status: "completed",
|
||||||
|
startedAt: 1000,
|
||||||
|
endedAt: 2000,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: "long-command",
|
||||||
|
tool: "bash",
|
||||||
|
title: "运行本地分析",
|
||||||
|
status: "completed",
|
||||||
|
target,
|
||||||
|
startedAt: 1100,
|
||||||
|
endedAt: 1900,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "short-command",
|
||||||
|
tool: "tjwater_cli",
|
||||||
|
title: "查询后端数据",
|
||||||
|
status: "completed",
|
||||||
|
target: "help data",
|
||||||
|
startedAt: 1900,
|
||||||
|
endedAt: 1950,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "展开分析过程" }));
|
||||||
|
expect(screen.getByText(/import json, glob/u)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/fourth line/u)).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText("help data")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "展开完整内容" })).toHaveAttribute(
|
||||||
|
"aria-expanded",
|
||||||
|
"false",
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "展开完整内容" }));
|
||||||
|
expect(screen.getByText(/fourth line/u)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "收起内容" })).toHaveAttribute(
|
||||||
|
"aria-expanded",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "收起内容" }));
|
||||||
|
expect(screen.queryByText(/fourth line/u)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Collapse,
|
Collapse,
|
||||||
IconButton,
|
IconButton,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
@@ -89,6 +90,11 @@ const ActionStatusIcon = ({ status }: { status: AgentActivityAction["status"] })
|
|||||||
|
|
||||||
const ActionRow = ({ action, now }: { action: AgentActivityAction; now: number }) => {
|
const ActionRow = ({ action, now }: { action: AgentActivityAction; now: number }) => {
|
||||||
const elapsed = getElapsedMs(action, now);
|
const elapsed = getElapsedMs(action, now);
|
||||||
|
const [targetExpanded, setTargetExpanded] = useState(false);
|
||||||
|
const targetId = React.useId();
|
||||||
|
const target = action.target ?? "";
|
||||||
|
const targetPreview = target.split(/\r?\n/u).slice(0, 3).join("\n").slice(0, 240);
|
||||||
|
const hasLongTarget = targetPreview.length < target.length;
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
@@ -111,19 +117,45 @@ const ActionRow = ({ action, now }: { action: AgentActivityAction; now: number }
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
{action.target ? (
|
{action.target ? (
|
||||||
<Typography
|
<Box sx={{ mt: 0.2 }}>
|
||||||
variant="caption"
|
<Typography
|
||||||
color="text.secondary"
|
id={targetId}
|
||||||
sx={{
|
variant="caption"
|
||||||
display: "block",
|
color="text.secondary"
|
||||||
mt: 0.2,
|
sx={{
|
||||||
fontFamily: "monospace",
|
display: !targetExpanded && hasLongTarget ? "-webkit-box" : "block",
|
||||||
wordBreak: "break-word",
|
fontFamily: "monospace",
|
||||||
whiteSpace: "pre-wrap",
|
overflow: !targetExpanded && hasLongTarget ? "hidden" : "visible",
|
||||||
}}
|
overflowWrap: "anywhere",
|
||||||
>
|
whiteSpace: "pre-wrap",
|
||||||
{action.target}
|
...(!targetExpanded && hasLongTarget
|
||||||
</Typography>
|
? { WebkitBoxOrient: "vertical", WebkitLineClamp: 3 }
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{targetExpanded ? target : targetPreview}
|
||||||
|
</Typography>
|
||||||
|
{hasLongTarget ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
aria-controls={targetId}
|
||||||
|
aria-expanded={targetExpanded}
|
||||||
|
onClick={() => setTargetExpanded((current) => !current)}
|
||||||
|
sx={{
|
||||||
|
mt: 0.2,
|
||||||
|
p: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
color: activityAccent,
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
fontWeight: 650,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
textTransform: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{targetExpanded ? "收起内容" : "展开完整内容"}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
{action.error ? (
|
{action.error ? (
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const approvalModeOptions: ReadonlyArray<{
|
|||||||
{
|
{
|
||||||
value: "always",
|
value: "always",
|
||||||
label: "始终允许",
|
label: "始终允许",
|
||||||
description: "除明确禁止项外自动放行",
|
description: "常规权限自动放行,敏感路径仍需确认",
|
||||||
icon: WarningAmberRounded,
|
icon: WarningAmberRounded,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
import { PermissionRequestGroup } from "./AgentPermissionRequests";
|
||||||
|
|
||||||
|
jest.mock("framer-motion", () => ({
|
||||||
|
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||||
|
motion: {
|
||||||
|
div: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div {...props}>{children}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("PermissionRequestGroup", () => {
|
||||||
|
it("shows an external path approval request and forwards the user's decision", () => {
|
||||||
|
const onReply = jest.fn();
|
||||||
|
render(
|
||||||
|
<ThemeProvider theme={createTheme()}>
|
||||||
|
<PermissionRequestGroup
|
||||||
|
permissions={[
|
||||||
|
{
|
||||||
|
requestId: "external-1",
|
||||||
|
sessionId: "session-1",
|
||||||
|
permission: "external_directory",
|
||||||
|
patterns: ["/app/private/*"],
|
||||||
|
target: "/app/private/report.json",
|
||||||
|
always: ["/app/private/*"],
|
||||||
|
createdAt: 1,
|
||||||
|
status: "pending",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
isRunning
|
||||||
|
onReply={onReply}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("访问工作区外目录")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("/app/private/report.json")).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "允许一次" }));
|
||||||
|
expect(onReply).toHaveBeenCalledWith("external-1", "once");
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "保存授权" }));
|
||||||
|
expect(onReply).toHaveBeenCalledWith("external-1", "always");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { parseApplyLayerStylePayload } from "./toolCallStyleHelpers";
|
import { parseApplyLayerStylePayload } from "./toolCallStyleHelpers";
|
||||||
|
import { createDefaultLayerStyleState } from "@components/olmap/core/Controls/styleEditorPresets";
|
||||||
|
import { validateStyleConfig } from "@components/olmap/core/Controls/styleEditorUtils";
|
||||||
|
|
||||||
describe("parseApplyLayerStylePayload", () => {
|
describe("parseApplyLayerStylePayload", () => {
|
||||||
it("accepts a valid snake_case interval contract", () => {
|
it("accepts a valid snake_case interval contract", () => {
|
||||||
@@ -47,4 +49,17 @@ describe("parseApplyLayerStylePayload", () => {
|
|||||||
}),
|
}),
|
||||||
).toMatchObject({ layerId: "junctions", styleConfig: { opacity: 0.5 } });
|
).toMatchObject({ layerId: "junctions", styleConfig: { opacity: 0.5 } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves default palette indexes when only some style fields are provided", () => {
|
||||||
|
const payload = parseApplyLayerStylePayload({
|
||||||
|
layer_id: "pipes",
|
||||||
|
style_config: { property: "flow", color_type: "gradient" },
|
||||||
|
});
|
||||||
|
expect(payload).not.toBeNull();
|
||||||
|
const merged = {
|
||||||
|
...createDefaultLayerStyleState("pipes").styleConfig,
|
||||||
|
...payload?.styleConfig,
|
||||||
|
};
|
||||||
|
expect(validateStyleConfig(merged)).toEqual({ valid: true, errors: [] });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -198,10 +198,15 @@ export const parseApplyLayerStylePayload = (
|
|||||||
) return null;
|
) return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const definedStyleConfig = styleConfig
|
||||||
|
? (Object.fromEntries(
|
||||||
|
Object.entries(styleConfig).filter(([, value]) => value !== undefined),
|
||||||
|
) as Partial<StyleConfig>)
|
||||||
|
: undefined;
|
||||||
const hasStyleOverrides =
|
const hasStyleOverrides =
|
||||||
styleConfig &&
|
definedStyleConfig &&
|
||||||
Object.values(styleConfig).some((value) =>
|
Object.values(definedStyleConfig).some((value) =>
|
||||||
Array.isArray(value) ? value.length > 0 : value !== undefined,
|
Array.isArray(value) ? value.length > 0 : true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!resetToDefault && !hasStyleOverrides) {
|
if (!resetToDefault && !hasStyleOverrides) {
|
||||||
@@ -211,7 +216,7 @@ export const parseApplyLayerStylePayload = (
|
|||||||
return {
|
return {
|
||||||
layerId,
|
layerId,
|
||||||
resetToDefault,
|
resetToDefault,
|
||||||
styleConfig: hasStyleOverrides ? styleConfig : undefined,
|
styleConfig: hasStyleOverrides ? definedStyleConfig : undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user