Compare commits

...
Author SHA1 Message Date
jiang 1b275206a9 fix(chat): collapse long activity commands
Generic Container CI/CD / test-build-publish (push) Successful in 1m32s
Frontend CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m32s
2026-09-25 16:23:00 +08:00
jiang cdf97afbc4 fix(chat): preserve style defaults for omitted agent overrides
Generic Container CI/CD / test-build-publish (push) Successful in 1m6s
Frontend CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m6s
2026-09-25 16:02:10 +08:00
4 changed files with 138 additions and 17 deletions
@@ -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();
});
});
+45 -13
View File
@@ -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
@@ -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: [] });
});
}); });
+9 -4
View File
@@ -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,
}; };
}; };