merge(cli): heure des messages figée à 1:00 — #221 (QA verte)
This commit is contained in:
@ -288,6 +288,66 @@ describe("CustomAgentChatView", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("replays scrollback with the current reception time instead of the reduce index", async () => {
|
||||||
|
const replayReceivedAtMs = new Date("2026-08-15T13:08:00.000Z").getTime();
|
||||||
|
const indexOneTime = expectedChatTime(1);
|
||||||
|
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(replayReceivedAtMs);
|
||||||
|
const agent = {
|
||||||
|
launchAgentChat: vi.fn(),
|
||||||
|
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||||
|
sessionId,
|
||||||
|
scrollback: [
|
||||||
|
{ kind: "userPrompt" as const, text: "Replay prompt" },
|
||||||
|
{ kind: "textDelta" as const, text: "Replay answer" },
|
||||||
|
{ kind: "final" as const, content: "Replay completed" },
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
sendAgentChat: vi.fn(async () => {}),
|
||||||
|
cancelAgentChat: vi.fn(async () => {}),
|
||||||
|
closeAgentChat: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent,
|
||||||
|
system: { pickFile: vi.fn(async () => null) },
|
||||||
|
} as unknown as Gateways}
|
||||||
|
>
|
||||||
|
<CustomAgentChatView
|
||||||
|
projectId="project-1"
|
||||||
|
agentId="agent-1"
|
||||||
|
agentName="Worker"
|
||||||
|
profile={profile}
|
||||||
|
cwd="/repo"
|
||||||
|
nodeId="node-1"
|
||||||
|
sessionId="chat-session-1"
|
||||||
|
conversationId="conversation-1"
|
||||||
|
onSessionId={vi.fn()}
|
||||||
|
onConversationId={vi.fn()}
|
||||||
|
/>
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const agentBubble = (await screen.findByText("Replay answer")).closest("div");
|
||||||
|
const finalBubble = screen.getByText("Replay completed").closest("div");
|
||||||
|
|
||||||
|
expect(agentBubble).toBeTruthy();
|
||||||
|
expect(finalBubble).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
within(agentBubble!).getByText(expectedChatTime(replayReceivedAtMs)),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
within(finalBubble!).getByText(expectedChatTime(replayReceivedAtMs)),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(within(agentBubble!).queryByText(indexOneTime)).toBeNull();
|
||||||
|
expect(within(finalBubble!).queryByText(indexOneTime)).toBeNull();
|
||||||
|
} finally {
|
||||||
|
nowSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("resizes the composer vertically with mouse, keyboard, and double-click reset", async () => {
|
it("resizes the composer vertically with mouse, keyboard, and double-click reset", async () => {
|
||||||
const rectSpy = vi
|
const rectSpy = vi
|
||||||
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
|
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
|
||||||
|
|||||||
@ -75,6 +75,14 @@ const chatConversationViewCache = new Map<string, CachedChatView>();
|
|||||||
const EMPTY_MODEL_RESPONSE_MESSAGE = "Le modèle n'a renvoyé aucune réponse.";
|
const EMPTY_MODEL_RESPONSE_MESSAGE = "Le modèle n'a renvoyé aucune réponse.";
|
||||||
const COMPOSER_RESIZE_STEP = 12;
|
const COMPOSER_RESIZE_STEP = 12;
|
||||||
const FALLBACK_COMPOSER_HEIGHT = 64;
|
const FALLBACK_COMPOSER_HEIGHT = 64;
|
||||||
|
const MIN_REASONABLE_CHAT_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
|
||||||
|
const MAX_REASONABLE_CHAT_TIMESTAMP_MS = Date.UTC(2100, 0, 1);
|
||||||
|
const MIN_REASONABLE_CHAT_TIMESTAMP_SECONDS = Math.floor(
|
||||||
|
MIN_REASONABLE_CHAT_TIMESTAMP_MS / 1000,
|
||||||
|
);
|
||||||
|
const MAX_REASONABLE_CHAT_TIMESTAMP_SECONDS = Math.floor(
|
||||||
|
MAX_REASONABLE_CHAT_TIMESTAMP_MS / 1000,
|
||||||
|
);
|
||||||
|
|
||||||
export function __clearCustomAgentChatViewCacheForTests() {
|
export function __clearCustomAgentChatViewCacheForTests() {
|
||||||
chatSessionViewCache.clear();
|
chatSessionViewCache.clear();
|
||||||
@ -1120,7 +1128,10 @@ export function CustomAgentChatView({
|
|||||||
setCurrentSession(reattached.sessionId);
|
setCurrentSession(reattached.sessionId);
|
||||||
if (options.applyScrollback) {
|
if (options.applyScrollback) {
|
||||||
scrollbackChunkCountRef.current = reattached.scrollback.length;
|
scrollbackChunkCountRef.current = reattached.scrollback.length;
|
||||||
const replayed = reattached.scrollback.reduce(foldChunk, emptyChatViewState());
|
const replayed = reattached.scrollback.reduce(
|
||||||
|
(state, chunk) => foldChunk(state, chunk),
|
||||||
|
emptyChatViewState(),
|
||||||
|
);
|
||||||
applyChatViewState(replayed);
|
applyChatViewState(replayed);
|
||||||
setBusy(turnsIndicateBusy(replayed));
|
setBusy(turnsIndicateBusy(replayed));
|
||||||
}
|
}
|
||||||
@ -2175,14 +2186,34 @@ function ChatBubble({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeChatReceivedAtMs(receivedAtMs: number | undefined): number | undefined {
|
||||||
|
if (!Number.isFinite(receivedAtMs) || receivedAtMs === undefined || receivedAtMs <= 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
receivedAtMs >= MIN_REASONABLE_CHAT_TIMESTAMP_SECONDS &&
|
||||||
|
receivedAtMs < MAX_REASONABLE_CHAT_TIMESTAMP_SECONDS
|
||||||
|
) {
|
||||||
|
return receivedAtMs * 1000;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
receivedAtMs < MIN_REASONABLE_CHAT_TIMESTAMP_MS ||
|
||||||
|
receivedAtMs >= MAX_REASONABLE_CHAT_TIMESTAMP_MS
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return receivedAtMs;
|
||||||
|
}
|
||||||
|
|
||||||
function formatChatReceivedTime(receivedAtMs: number | undefined): string {
|
function formatChatReceivedTime(receivedAtMs: number | undefined): string {
|
||||||
if (!Number.isFinite(receivedAtMs) || !receivedAtMs) return "";
|
const normalizedAtMs = normalizeChatReceivedAtMs(receivedAtMs);
|
||||||
|
if (!normalizedAtMs) return "";
|
||||||
try {
|
try {
|
||||||
const parts = new Intl.DateTimeFormat(undefined, {
|
const parts = new Intl.DateTimeFormat(undefined, {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hourCycle: "h23",
|
hourCycle: "h23",
|
||||||
}).formatToParts(new Date(receivedAtMs));
|
}).formatToParts(new Date(normalizedAtMs));
|
||||||
const hour = parts.find((part) => part.type === "hour")?.value;
|
const hour = parts.find((part) => part.type === "hour")?.value;
|
||||||
const minute = parts.find((part) => part.type === "minute")?.value;
|
const minute = parts.find((part) => part.type === "minute")?.value;
|
||||||
return hour && minute ? `${hour}:${minute}` : "";
|
return hour && minute ? `${hour}:${minute}` : "";
|
||||||
@ -2201,7 +2232,8 @@ function ChatMessageMeta({
|
|||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
}) {
|
}) {
|
||||||
const time = formatChatReceivedTime(receivedAtMs);
|
const time = formatChatReceivedTime(receivedAtMs);
|
||||||
const dateTime = time && receivedAtMs ? new Date(receivedAtMs).toISOString() : undefined;
|
const normalizedAtMs = normalizeChatReceivedAtMs(receivedAtMs);
|
||||||
|
const dateTime = time && normalizedAtMs ? new Date(normalizedAtMs).toISOString() : undefined;
|
||||||
return (
|
return (
|
||||||
<div className="mb-1 flex min-w-0 flex-col gap-0.5 text-xs sm:flex-row sm:items-baseline sm:justify-between sm:gap-3">
|
<div className="mb-1 flex min-w-0 flex-col gap-0.5 text-xs sm:flex-row sm:items-baseline sm:justify-between sm:gap-3">
|
||||||
<span className={cn("min-w-0 truncate font-medium text-muted", labelClassName)}>
|
<span className={cn("min-w-0 truncate font-medium text-muted", labelClassName)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user