From 545499aadd56ab45654178df9b1fd3e22cf5f881 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Mon, 18 May 2026 20:45:13 +0200 Subject: [PATCH] feat(ui): add rotating catch-up notices --- .changeset/friendly-reconnect-message.md | 5 +++++ src/client/src/components/ChatView.ts | 28 ++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .changeset/friendly-reconnect-message.md diff --git a/.changeset/friendly-reconnect-message.md b/.changeset/friendly-reconnect-message.md new file mode 100644 index 0000000..b4ac4fa --- /dev/null +++ b/.changeset/friendly-reconnect-message.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Add friendlier rotating in-progress response notices when opening a chat mid-reply. diff --git a/src/client/src/components/ChatView.ts b/src/client/src/components/ChatView.ts index ea38527..3e41ac3 100644 --- a/src/client/src/components/ChatView.ts +++ b/src/client/src/components/ChatView.ts @@ -12,6 +12,19 @@ import "./FormattedText"; const shortTimestampFormatter = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); const fullTimestampFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "medium" }); +const partialStreamNoticeBodies = [ + "You opened this chat while the assistant was already replying. The complete answer will appear shortly.", + "We joined mid-sentence. Holding the curtain until the full reply is ready.", + "The assistant started before this tab arrived. We’ll show the full answer when it lands.", + "Catching the reply in one piece — no spoilers, no half-answers.", + "The tokens are still assembling themselves. Full answer incoming.", + "We arrived fashionably late to this response. The complete version will appear soon.", +] as const; + +function randomPartialStreamNoticeBody(): string { + return partialStreamNoticeBodies[Math.floor(Math.random() * partialStreamNoticeBodies.length)] ?? partialStreamNoticeBodies[0]; +} + function isScrollPosition(value: unknown): value is { index?: number; key?: string; offset: number } { return typeof value === "object" && value !== null @@ -49,6 +62,7 @@ export class ChatView extends LitElement { private groupedMessagesCache: ChatGroup[] = []; private readonly messageMetaCache = new WeakMap(); private readonly messageCopyTextCache = new WeakMap(); + private partialStreamNoticeBody: string | undefined; private lastScrollTop = 0; private lastClientHeight = 0; private touchStartY: number | undefined; @@ -79,6 +93,7 @@ export class ChatView extends LitElement { protected override willUpdate(changed: Map): void { if (changed.has("sessionId")) this.openGroupKeys = this.readOpenGroupKeys(); + if (changed.has("isReceivingPartialStream") || (changed.has("sessionId") && this.isReceivingPartialStream)) this.syncPartialStreamNoticeBody(); if (changed.has("messages")) this.pinnedToBottom = this.pinnedToBottom && (this.didChatHeightChange() || this.isNearBottom()); } @@ -158,8 +173,8 @@ export class ChatView extends LitElement { private renderSessionActivity() { if (this.isReceivingPartialStream) return html` `; if (!this.isCompacting) return null; @@ -172,6 +187,15 @@ export class ChatView extends LitElement { `; } + private syncPartialStreamNoticeBody(): void { + this.partialStreamNoticeBody = this.isReceivingPartialStream ? randomPartialStreamNoticeBody() : undefined; + } + + private currentPartialStreamNoticeBody(): string { + this.partialStreamNoticeBody ??= randomPartialStreamNoticeBody(); + return this.partialStreamNoticeBody; + } + private activityState(): string | undefined { const status = this.status; if (status === undefined) return this.activity?.label;