feat(webui): add project workspaces and access controls (#4007)

* feat(webui): add project workspaces and access controls

* feat(webui): add project workspaces and access controls

* refactor(tools): centralize workspace access resolution

* refactor(webui): remove unused workspace host state

* fix(webui): hide estimated file edit label

* fix(webui): clarify file edit deletion feedback

* fix(webui): label deleted file activity

* fix(webui): flatten file edit activity rows

* fix(core): remove path-only patch deletion

* fix(core): keep apply patch non-destructive

* refactor(webui): trim workspace host plumbing

* fix(tools): register exec with tools config
This commit is contained in:
Xubin Ren
2026-05-29 03:42:53 +08:00
committed by GitHub
parent 84428136e6
commit 3a420136bb
111 changed files with 9972 additions and 1822 deletions
@@ -433,6 +433,72 @@ describe("AgentActivityCluster", () => {
}
});
it("labels whole-file deletes as deleted instead of edited", () => {
render(
<AgentActivityCluster
messages={activityMessages("", {
id: "t-delete",
role: "tool",
kind: "trace",
content: "apply_patch()",
traces: ["apply_patch()"],
fileEdits: [{
call_id: "call-delete",
tool: "apply_patch",
path: "angry-birds.html",
phase: "end",
added: 0,
deleted: 590,
approximate: false,
status: "done",
operation: "delete",
}],
createdAt: 3,
})}
isTurnStreaming={false}
hasBodyBelow={false}
/>,
);
expect(screen.getByRole("button", { name: /deleted angry-birds\.html/i })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /edited angry-birds\.html/i })).not.toBeInTheDocument();
});
it("renders file-only edits without a redundant disclosure", () => {
render(
<AgentActivityCluster
messages={[{
id: "t-file-only",
role: "tool",
kind: "trace",
content: "apply_patch()",
traces: ["apply_patch()"],
fileEdits: [{
call_id: "call-patch",
tool: "apply_patch",
path: "src/app.tsx",
absolute_path: "/Users/renxubin/project/src/app.tsx",
phase: "end",
added: 12,
deleted: 3,
approximate: false,
status: "done",
}],
createdAt: 3,
}]}
isTurnStreaming={false}
hasBodyBelow={false}
/>,
);
expect(screen.queryByRole("button", { name: /edited app\.tsx/i })).not.toBeInTheDocument();
expect(screen.queryByTestId("agent-activity-scroll")).not.toBeInTheDocument();
expect(screen.getByText("Edited")).toBeInTheDocument();
expect(screen.getByTestId("activity-header-file-reference")).toHaveTextContent("app.tsx");
expect(screen.getByText("+12")).toBeInTheDocument();
expect(screen.getByText("-3")).toBeInTheDocument();
});
it("renders CLI app runs as dedicated activity rows", () => {
const line = 'run_cli_app({"name":"blender","args":["--background","scene.blend"],"json":true})';
render(
@@ -771,6 +837,38 @@ describe("AgentActivityCluster", () => {
expect(screen.getByText("Preparing file edit…")).toBeInTheDocument();
});
it("shows the reason when a file edit fails", () => {
render(
<AgentActivityCluster
messages={activityMessages("", {
id: "t2",
role: "tool",
kind: "trace",
content: "apply_patch()",
traces: ["apply_patch()"],
fileEdits: [{
call_id: "call-patch",
tool: "apply_patch",
path: "angry-birds.html",
phase: "error",
added: 0,
deleted: 0,
approximate: false,
status: "error",
error: "Error applying patch: old_text not found in angry-birds.html",
}],
createdAt: 3,
})}
isTurnStreaming={false}
hasBodyBelow={false}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /failed angry-birds\.html/i }));
expect(screen.getByText("Target text was not found in angry-birds.html.")).toBeInTheDocument();
});
it("merges repeated edits for the same path and lets successful edits win over failures", async () => {
const restoreMotion = installReducedMotion();
try {