feat(webui): refine output timeline and model controls (#4108)
* feat(webui): refine output timeline and composer queue * feat(webui): add provider model picker * fix(webui): polish model settings and heartbeat checks * chore: keep heartbeat changes out of webui pr * refactor(webui): isolate settings routes * fix(providers): align minimax anthropic test * fix(providers): keep minimax anthropic base sdk-compatible * fix(providers): normalize anthropic base urls
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
import type { UIMessage } from "@/lib/types";
|
||||
|
||||
describe("ThreadMessages", () => {
|
||||
it("groups consecutive reasoning and tool rows into one cluster before the answer", () => {
|
||||
it("groups consecutive reasoning and tool rows into one timeline before the answer", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
id: "r1",
|
||||
@@ -55,7 +55,7 @@ describe("ThreadMessages", () => {
|
||||
expect(rows[1]).toHaveClass("mt-4");
|
||||
});
|
||||
|
||||
it("starts a new activity cluster when the activity segment changes", () => {
|
||||
it("keeps file edits as their own activity row inside a turn", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
id: "r1",
|
||||
@@ -95,14 +95,11 @@ describe("ThreadMessages", () => {
|
||||
|
||||
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].type === "cluster" ? units[1].messages.map((m) => m.id) : []).toEqual([
|
||||
"r2",
|
||||
]);
|
||||
expect(units).toHaveLength(3);
|
||||
expect(units.map((unit) => unit.type)).toEqual(["activity", "activity", "activity"]);
|
||||
expect(units[0].type === "activity" ? units[0].messages.map((m) => m.id) : []).toEqual(["r1"]);
|
||||
expect(units[1].type === "activity" ? units[1].messages.map((m) => m.id) : []).toEqual(["t1"]);
|
||||
expect(units[2].type === "activity" ? units[2].messages.map((m) => m.id) : []).toEqual(["r2"]);
|
||||
});
|
||||
|
||||
it("does not split ordinary tool activity just because segment ids changed", () => {
|
||||
@@ -146,7 +143,7 @@ describe("ThreadMessages", () => {
|
||||
const units = buildDisplayUnits(messages);
|
||||
|
||||
expect(units).toHaveLength(1);
|
||||
expect(units[0].type === "cluster" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
expect(units[0].type === "activity" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
"r1",
|
||||
"t1",
|
||||
"r2",
|
||||
@@ -154,7 +151,7 @@ describe("ThreadMessages", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("only marks the current activity cluster as live while streaming", () => {
|
||||
it("only marks the current activity timeline as live while streaming", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
id: "r1",
|
||||
@@ -197,12 +194,10 @@ describe("ThreadMessages", () => {
|
||||
|
||||
render(<ThreadMessages messages={messages} isStreaming />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /edited foo\.txt/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /editing foo\.txt/i })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /working/i })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/editing foo\.txt/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("folds final answer reasoning into the preceding activity cluster", () => {
|
||||
it("folds final answer reasoning into the preceding activity timeline", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
id: "r1",
|
||||
@@ -234,21 +229,21 @@ describe("ThreadMessages", () => {
|
||||
const units = buildDisplayUnits(messages);
|
||||
|
||||
expect(units).toHaveLength(2);
|
||||
expect(units[0]).toMatchObject({ type: "cluster" });
|
||||
expect(units[0].type === "cluster" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
expect(units[0]).toMatchObject({ type: "activity" });
|
||||
expect(units[0].type === "activity" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
"r1",
|
||||
"t1",
|
||||
"a1-reasoning",
|
||||
]);
|
||||
expect(units[0].type === "cluster" ? units[0].messages.at(-1)?.latencyMs : undefined).toBe(9_200);
|
||||
expect(units[0].type === "activity" ? units[0].messages.at(-1)?.latencyMs : undefined).toBe(9_200);
|
||||
expect(units[1]).toMatchObject({
|
||||
type: "single",
|
||||
type: "message",
|
||||
message: {
|
||||
id: "a1",
|
||||
content: "final answer",
|
||||
},
|
||||
});
|
||||
if (units[1].type === "single") {
|
||||
if (units[1].type === "message") {
|
||||
expect(units[1].message).not.toHaveProperty("reasoning");
|
||||
}
|
||||
|
||||
@@ -290,12 +285,12 @@ describe("ThreadMessages", () => {
|
||||
const units = buildDisplayUnits(messages);
|
||||
|
||||
expect(units).toHaveLength(2);
|
||||
expect(units[0].type === "cluster" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
expect(units[0].type === "activity" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
"t0",
|
||||
"t1",
|
||||
]);
|
||||
expect(units[1]).toMatchObject({
|
||||
type: "single",
|
||||
type: "message",
|
||||
message: {
|
||||
id: "a1",
|
||||
content: "partial answer",
|
||||
@@ -340,12 +335,12 @@ describe("ThreadMessages", () => {
|
||||
const units = buildDisplayUnits(messages);
|
||||
|
||||
expect(units).toHaveLength(2);
|
||||
expect(units[0].type === "cluster" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
expect(units[0].type === "activity" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
"r1",
|
||||
"t1",
|
||||
]);
|
||||
expect(units[1]).toMatchObject({
|
||||
type: "single",
|
||||
type: "message",
|
||||
message: {
|
||||
id: "a1",
|
||||
content: "Hong Kong is hot today.",
|
||||
@@ -391,12 +386,12 @@ describe("ThreadMessages", () => {
|
||||
const units = buildDisplayUnits(messages);
|
||||
|
||||
expect(units).toHaveLength(2);
|
||||
expect(units[0].type === "cluster" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
expect(units[0].type === "activity" ? units[0].messages.map((m) => m.id) : []).toEqual([
|
||||
"prelude",
|
||||
"tool",
|
||||
]);
|
||||
expect(units[1]).toMatchObject({
|
||||
type: "single",
|
||||
type: "message",
|
||||
message: {
|
||||
id: "final",
|
||||
content: "Done. Open index.html to play.",
|
||||
@@ -404,7 +399,7 @@ describe("ThreadMessages", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes assistant turn latency to the preceding completed activity cluster", () => {
|
||||
it("passes assistant turn latency to the preceding completed activity timeline", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
id: "r1",
|
||||
@@ -496,7 +491,7 @@ describe("ThreadMessages", () => {
|
||||
const flags = assistantCopyFlags(units);
|
||||
const assistantFlags = units
|
||||
.map((unit, index) =>
|
||||
unit.type === "single" && unit.message.role === "assistant"
|
||||
unit.type === "message" && unit.message.role === "assistant"
|
||||
? [unit.message.id, flags[index]]
|
||||
: null,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user