History

fix: Fix response duration for agent to no longer always be 1ms (#866)

Jono Kemball committed 8 months ago

Commit ed4be2c1 parent 5e23e9c3 files · +169 −2Browse files

Unified · Split

3 files
  1. apps/web/src/components/chat/MessagesTimeline.logic.test.ts +135 −0
  2. apps/web/src/components/chat/MessagesTimeline.logic.ts +25 −0
  3. apps/web/src/components/chat/MessagesTimeline.tsx +9 −2
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+135−0
@@ -0,0 +1,135 @@
1+import { describe, expect, it } from "vitest";
2+import { computeMessageDurationStart } from "./MessagesTimeline.logic";
3+
4+describe("computeMessageDurationStart", () => {
5+ it("returns message createdAt when there is no preceding user message", () => {
6+ const result = computeMessageDurationStart([
7+ {
8+ id: "a1",
9+ role: "assistant",
10+ createdAt: "2026-01-01T00:00:05Z",
11+ completedAt: "2026-01-01T00:00:10Z",
12+ },
13+ ]);
14+ expect(result).toEqual(new Map([["a1", "2026-01-01T00:00:05Z"]]));
15+ });
16+
17+ it("uses the user message createdAt for the first assistant response", () => {
18+ const result = computeMessageDurationStart([
19+ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },
20+ {
21+ id: "a1",
22+ role: "assistant",
23+ createdAt: "2026-01-01T00:00:30Z",
24+ completedAt: "2026-01-01T00:00:30Z",
25+ },
26+ ]);
27+
28+ expect(result).toEqual(
29+ new Map([
30+ ["u1", "2026-01-01T00:00:00Z"],
31+ ["a1", "2026-01-01T00:00:00Z"],
32+ ]),
33+ );
34+ });
35+
36+ it("uses the previous assistant completedAt for subsequent assistant responses", () => {
37+ const result = computeMessageDurationStart([
38+ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },
39+ {
40+ id: "a1",
41+ role: "assistant",
42+ createdAt: "2026-01-01T00:00:30Z",
43+ completedAt: "2026-01-01T00:00:30Z",
44+ },
45+ {
46+ id: "a2",
47+ role: "assistant",
48+ createdAt: "2026-01-01T00:00:55Z",
49+ completedAt: "2026-01-01T00:00:55Z",
50+ },
51+ ]);
52+
53+ expect(result).toEqual(
54+ new Map([
55+ ["u1", "2026-01-01T00:00:00Z"],
56+ ["a1", "2026-01-01T00:00:00Z"],
57+ ["a2", "2026-01-01T00:00:30Z"],
58+ ]),
59+ );
60+ });
61+
62+ it("does not advance the boundary for a streaming message without completedAt", () => {
63+ const result = computeMessageDurationStart([
64+ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },
65+ { id: "a1", role: "assistant", createdAt: "2026-01-01T00:00:30Z" },
66+ {
67+ id: "a2",
68+ role: "assistant",
69+ createdAt: "2026-01-01T00:00:55Z",
70+ completedAt: "2026-01-01T00:00:55Z",
71+ },
72+ ]);
73+
74+ expect(result).toEqual(
75+ new Map([
76+ ["u1", "2026-01-01T00:00:00Z"],
77+ ["a1", "2026-01-01T00:00:00Z"],
78+ ["a2", "2026-01-01T00:00:00Z"],
79+ ]),
80+ );
81+ });
82+
83+ it("resets the boundary on a new user message", () => {
84+ const result = computeMessageDurationStart([
85+ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },
86+ {
87+ id: "a1",
88+ role: "assistant",
89+ createdAt: "2026-01-01T00:00:30Z",
90+ completedAt: "2026-01-01T00:00:30Z",
91+ },
92+ { id: "u2", role: "user", createdAt: "2026-01-01T00:01:00Z" },
93+ {
94+ id: "a2",
95+ role: "assistant",
96+ createdAt: "2026-01-01T00:01:20Z",
97+ completedAt: "2026-01-01T00:01:20Z",
98+ },
99+ ]);
100+
101+ expect(result).toEqual(
102+ new Map([
103+ ["u1", "2026-01-01T00:00:00Z"],
104+ ["a1", "2026-01-01T00:00:00Z"],
105+ ["u2", "2026-01-01T00:01:00Z"],
106+ ["a2", "2026-01-01T00:01:00Z"],
107+ ]),
108+ );
109+ });
110+
111+ it("handles system messages without affecting the boundary", () => {
112+ const result = computeMessageDurationStart([
113+ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },
114+ { id: "s1", role: "system", createdAt: "2026-01-01T00:00:01Z" },
115+ {
116+ id: "a1",
117+ role: "assistant",
118+ createdAt: "2026-01-01T00:00:30Z",
119+ completedAt: "2026-01-01T00:00:30Z",
120+ },
121+ ]);
122+
123+ expect(result).toEqual(
124+ new Map([
125+ ["u1", "2026-01-01T00:00:00Z"],
126+ ["s1", "2026-01-01T00:00:00Z"],
127+ ["a1", "2026-01-01T00:00:00Z"],
128+ ]),
129+ );
130+ });
131+
132+ it("returns empty map for empty input", () => {
133+ expect(computeMessageDurationStart([])).toEqual(new Map());
134+ });
135+});
apps/web/src/components/chat/MessagesTimeline.logic.ts+25−0
@@ -0,0 +1,25 @@
1+export interface TimelineDurationMessage {
2+ id: string;
3+ role: "user" | "assistant" | "system";
4+ createdAt: string;
5+ completedAt?: string | undefined;
6+}
7+
8+export function computeMessageDurationStart(
9+ messages: ReadonlyArray<TimelineDurationMessage>,
10+): Map<string, string> {
11+ const result = new Map<string, string>();
12+ let lastBoundary: string | null = null;
13+
14+ for (const message of messages) {
15+ if (message.role === "user") {
16+ lastBoundary = message.createdAt;
17+ }
18+ result.set(message.id, lastBoundary ?? message.createdAt);
19+ if (message.role === "assistant" && message.completedAt) {
20+ lastBoundary = message.completedAt;
21+ }
22+ }
23+
24+ return result;
25+}
apps/web/src/components/chat/MessagesTimeline.tsx+9−2
@@ -19,6 +19,7 @@ import { ProposedPlanCard } from "./ProposedPlanCard"; · 18 unmodified lines
1919 import { ChangedFilesTree } from "./ChangedFilesTree";
2020 import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel";
2121 import { MessageCopyButton } from "./MessageCopyButton";
22+import { computeMessageDurationStart } from "./MessagesTimeline.logic";
2223
2324 const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;
2425 const ALWAYS_UNVIRTUALIZED_TAIL_ROWS = 8;
@@ -98,6 +99,9 @@ export const MessagesTimeline = memo(function MessagesTimeline({ · 73 unmodified lines
9899
99100 const rows = useMemo<TimelineRow[]>(() => {
100101 const nextRows: TimelineRow[] = [];
102+ const durationStartByMessageId = computeMessageDurationStart(
103+ timelineEntries.flatMap((entry) => (entry.kind === "message" ? [entry.message] : [])),
104+ );
101105
102106 for (let index = 0; index < timelineEntries.length; index += 1) {
103107 const timelineEntry = timelineEntries[index];
@@ -139,6 +143,8 @@ export const MessagesTimeline = memo(function MessagesTimeline({ · 35 unmodified lines
139143 id: timelineEntry.id,
140144 createdAt: timelineEntry.createdAt,
141145 message: timelineEntry.message,
146+ durationStart:
147+ durationStartByMessageId.get(timelineEntry.message.id) ?? timelineEntry.message.createdAt,
142148 showCompletionDivider:
143149 timelineEntry.message.role === "assistant" &&
144150 completionDividerBeforeEntryId === timelineEntry.id,
@@ -507,8 +513,8 @@ export const MessagesTimeline = memo(function MessagesTimeline({ · 362 unmodified lines
507513 {formatMessageMeta(
508514 row.message.createdAt,
509515 row.message.streaming
510− ? formatElapsed(row.message.createdAt, nowIso)
511− : formatElapsed(row.message.createdAt, row.message.completedAt),
516+ ? formatElapsed(row.durationStart, nowIso)
517+ : formatElapsed(row.durationStart, row.message.completedAt),
512518 )}
513519 </p>
514520 </div>
@@ -605,6 +611,7 @@ type TimelineRow = · 90 unmodified lines
605611 id: string;
606612 createdAt: string;
607613 message: TimelineMessage;
614+ durationStart: string;
608615 showCompletionDivider: boolean;
609616 }
610617 | {