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:
@@ -258,6 +258,152 @@ describe("ThreadMessages", () => {
|
||||
expect(screen.getByText("final answer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps late activity above the live assistant answer while streaming", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
id: "t0",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: "Thinking",
|
||||
traces: ["Thinking"],
|
||||
activitySegmentId: "seg-live",
|
||||
createdAt: 1,
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
role: "assistant",
|
||||
content: "partial answer",
|
||||
isStreaming: true,
|
||||
createdAt: 2,
|
||||
},
|
||||
{
|
||||
id: "t1",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: "Reading api.github.com/repos/NousResearch/hermes-agent",
|
||||
traces: ["Reading api.github.com/repos/NousResearch/hermes-agent"],
|
||||
activitySegmentId: "seg-live",
|
||||
createdAt: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const units = buildDisplayUnits(messages);
|
||||
|
||||
expect(units).toHaveLength(2);
|
||||
expect(units[0].type === "cluster" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
"t0",
|
||||
"t1",
|
||||
]);
|
||||
expect(units[1]).toMatchObject({
|
||||
type: "single",
|
||||
message: {
|
||||
id: "a1",
|
||||
content: "partial answer",
|
||||
},
|
||||
});
|
||||
|
||||
render(<ThreadMessages messages={messages} isStreaming />);
|
||||
|
||||
const activity = screen.getByRole("button", { name: /working/i });
|
||||
const answer = screen.getByText("partial answer");
|
||||
expect(activity.compareDocumentPosition(answer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps late activity above a completed assistant answer", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
id: "r1",
|
||||
role: "assistant",
|
||||
content: "",
|
||||
reasoning: "checking weather",
|
||||
activitySegmentId: "seg-late",
|
||||
createdAt: 1,
|
||||
},
|
||||
{
|
||||
id: "a1",
|
||||
role: "assistant",
|
||||
content: "Hong Kong is hot today.",
|
||||
latencyMs: 161_000,
|
||||
createdAt: 2,
|
||||
},
|
||||
{
|
||||
id: "t1",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: "Reading hko.gov.hk/en/wxinfo/currwx/current.htm",
|
||||
traces: ["Reading hko.gov.hk/en/wxinfo/currwx/current.htm"],
|
||||
activitySegmentId: "seg-late",
|
||||
createdAt: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const units = buildDisplayUnits(messages);
|
||||
|
||||
expect(units).toHaveLength(2);
|
||||
expect(units[0].type === "cluster" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
"r1",
|
||||
"t1",
|
||||
]);
|
||||
expect(units[1]).toMatchObject({
|
||||
type: "single",
|
||||
message: {
|
||||
id: "a1",
|
||||
content: "Hong Kong is hot today.",
|
||||
},
|
||||
});
|
||||
|
||||
render(<ThreadMessages messages={messages} isStreaming={false} />);
|
||||
|
||||
const activity = screen.getByText("Thought for 2m 41s");
|
||||
const answer = screen.getByText("Hong Kong is hot today.");
|
||||
expect(activity.compareDocumentPosition(answer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(screen.getAllByText(/thought/i)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders interrupted pre-tool text as activity before the final answer", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
id: "prelude",
|
||||
role: "assistant",
|
||||
content: "",
|
||||
reasoning: "I will inspect first.",
|
||||
isStreaming: false,
|
||||
activitySegmentId: "seg-1",
|
||||
createdAt: 1,
|
||||
},
|
||||
{
|
||||
id: "tool",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: 'exec({"cmd":"ls"})',
|
||||
traces: ['exec({"cmd":"ls"})'],
|
||||
activitySegmentId: "seg-1",
|
||||
createdAt: 2,
|
||||
},
|
||||
{
|
||||
id: "final",
|
||||
role: "assistant",
|
||||
content: "Done. Open index.html to play.",
|
||||
createdAt: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const units = buildDisplayUnits(messages);
|
||||
|
||||
expect(units).toHaveLength(2);
|
||||
expect(units[0].type === "cluster" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
"prelude",
|
||||
"tool",
|
||||
]);
|
||||
expect(units[1]).toMatchObject({
|
||||
type: "single",
|
||||
message: {
|
||||
id: "final",
|
||||
content: "Done. Open index.html to play.",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("passes assistant turn latency to the preceding completed activity cluster", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user