From 3bd4773c8791a047310cd3d437334a5b137c2797 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Thu, 28 May 2026 14:19:44 +0200 Subject: [PATCH] fix: show raw chat history range --- .changeset/fix-chat-history-range.md | 5 +++++ src/client/src/appState.ts | 2 ++ src/client/src/chatTranscriptStore.test.ts | 17 +++++++++++++++++ src/client/src/chatTranscriptStore.ts | 7 ++++++- src/client/src/components/ChatView.ts | 10 ++++++++-- src/client/src/components/PiWebApp.ts | 2 +- src/client/src/controllers/sessionController.ts | 2 +- 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-chat-history-range.md diff --git a/.changeset/fix-chat-history-range.md b/.changeset/fix-chat-history-range.md new file mode 100644 index 0000000..afeac10 --- /dev/null +++ b/.changeset/fix-chat-history-range.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Correct the chat history range label when normalized display messages are fewer than the raw session transcript entries. diff --git a/src/client/src/appState.ts b/src/client/src/appState.ts index 409bf73..100bfd3 100644 --- a/src/client/src/appState.ts +++ b/src/client/src/appState.ts @@ -8,6 +8,7 @@ export interface AppState { sessions: SessionInfo[]; messages: ChatLine[]; messagePageStart: number; + messagePageEnd: number; messagePageTotal: number; isLoadingEarlierMessages: boolean; isReceivingPartialStream: boolean; @@ -96,6 +97,7 @@ export function initialAppState(): AppState { sessions: [], messages: [], messagePageStart: 0, + messagePageEnd: 0, messagePageTotal: 0, isLoadingEarlierMessages: false, isReceivingPartialStream: false, diff --git a/src/client/src/chatTranscriptStore.test.ts b/src/client/src/chatTranscriptStore.test.ts index fcf5045..0bc5eb6 100644 --- a/src/client/src/chatTranscriptStore.test.ts +++ b/src/client/src/chatTranscriptStore.test.ts @@ -29,10 +29,26 @@ describe("ChatTranscriptStore", () => { { role: "assistant", parts: [{ type: "text", text: "hello" }] }, ], messagePageStart: 0, + messagePageEnd: 2, messagePageTotal: 2, }); }); + it("tracks the raw page end separately from normalized display messages", () => { + const store = new ChatTranscriptStore(new MemoryChatHistoryCache()); + + const view = store.mergeHistory("s1", page(0, 3, [ + { role: "user", content: "run the tool" }, + { role: "assistant", content: [{ type: "toolCall", id: "tool-1", name: "read", arguments: { path: "src/app.ts" } }] }, + { role: "toolResult", toolCallId: "tool-1", toolName: "read", content: [{ type: "text", text: "ok" }] }, + ])); + + expect(view.messages).toHaveLength(2); + expect(view.messagePageStart).toBe(0); + expect(view.messagePageEnd).toBe(3); + expect(view.messagePageTotal).toBe(3); + }); + it("keeps live streamed transcript state out of the raw history cache", () => { const cache = new MemoryChatHistoryCache(); const store = new ChatTranscriptStore(cache); @@ -51,6 +67,7 @@ describe("ChatTranscriptStore", () => { { role: "user", parts: [{ type: "text", text: "next" }] }, ], messagePageStart: 0, + messagePageEnd: 3, messagePageTotal: 3, }); }); diff --git a/src/client/src/chatTranscriptStore.ts b/src/client/src/chatTranscriptStore.ts index 1d696c4..cd8eb95 100644 --- a/src/client/src/chatTranscriptStore.ts +++ b/src/client/src/chatTranscriptStore.ts @@ -7,6 +7,9 @@ import type { SessionUiEvent } from "./sessionSocket"; export interface ChatTranscriptView { messages: ChatLine[]; messagePageStart: number; + // End offset in the raw transcript. Normalization may coalesce multiple raw + // entries into one displayed chat message, especially tool calls/results. + messagePageEnd: number; messagePageTotal: number; } @@ -48,9 +51,11 @@ export class ChatTranscriptStore { } export function transcriptViewFromHistory(history: RawMessagePage | undefined): ChatTranscriptView { + const start = history?.start ?? 0; return { messages: normalizeMessages(history?.messages ?? []), - messagePageStart: history?.start ?? 0, + messagePageStart: start, + messagePageEnd: start + (history?.messages.length ?? 0), messagePageTotal: history?.total ?? 0, }; } diff --git a/src/client/src/components/ChatView.ts b/src/client/src/components/ChatView.ts index 8b8d994..869e5a3 100644 --- a/src/client/src/components/ChatView.ts +++ b/src/client/src/components/ChatView.ts @@ -43,6 +43,7 @@ export class ChatView extends LitElement { @property({ attribute: false }) messages: ChatLine[] = []; @property() sessionId = ""; @property({ type: Number }) messageStart = 0; + @property({ type: Number }) messageEnd = 0; @property({ type: Number }) messageTotal = 0; @property({ type: Boolean }) hasMore = false; @property({ type: Boolean }) loadingMore = false; @@ -307,8 +308,13 @@ export class ChatView extends LitElement { private historyRangeLabel() { if (!this.messages.length || this.messageTotal <= 0) return null; const from = this.messageStart + 1; - const to = this.messageStart + this.messages.length; - return html`Showing messages ${from}–${to} of ${this.messageTotal}`; + const to = this.loadedRawMessageEnd(); + const total = Math.max(this.messageTotal, to); + return html`Showing messages ${from}–${to} of ${total}`; + } + + private loadedRawMessageEnd(): number { + return Math.max(this.messageEnd, this.messageStart + this.messages.length); } private renderMessage(message: ChatLine, index: number) { diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts index 14706c6..92329dd 100644 --- a/src/client/src/components/PiWebApp.ts +++ b/src/client/src/components/PiWebApp.ts @@ -1334,7 +1334,7 @@ export class PiWebApp extends LitElement { ${state.error ? html`
${state.error}
` : null}
${this.isMobileNavigationLayout ? this.renderNavigationPanel(true) : null}
${state.selectedSession ? html` - 0} .loadingMore=${state.isLoadingEarlierMessages} .isReceivingPartialStream=${state.isReceivingPartialStream} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .status=${state.status} .activity=${state.activity} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}> + 0} .loadingMore=${state.isLoadingEarlierMessages} .isReceivingPartialStream=${state.isReceivingPartialStream} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .status=${state.status} .activity=${state.activity} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}> 0} .status=${state.status} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp") => { this.sendPrompt(text, streamingBehavior); }} .onStop=${() => this.sessions.stopActiveWork()} .onSelectModel=${() => { void this.openModelDialog(); }} .onSelectThinking=${() => { void this.openThinkingDialog(); }}> ${state.commandDialog !== undefined ? html` this.sessions.respondToCommand(state.commandDialog?.requestId ?? "", value)} .onCancel=${() => { this.sessions.cancelCommand(); }}>` : null} diff --git a/src/client/src/controllers/sessionController.ts b/src/client/src/controllers/sessionController.ts index b8b8918..aeab86c 100644 --- a/src/client/src/controllers/sessionController.ts +++ b/src/client/src/controllers/sessionController.ts @@ -61,7 +61,7 @@ export class SessionController { this.socket.close(); this.catchupStreamSessionId = undefined; this.clearPendingTranscriptEvents(); - this.setState({ selectedSession: undefined, messages: [], messagePageStart: 0, messagePageTotal: 0, isLoadingEarlierMessages: false, isReceivingPartialStream: false, status: undefined, activity: undefined }); + this.setState({ selectedSession: undefined, messages: [], messagePageStart: 0, messagePageEnd: 0, messagePageTotal: 0, isLoadingEarlierMessages: false, isReceivingPartialStream: false, status: undefined, activity: undefined }); } deselectSession(options?: { forgetRememberedSelection?: boolean | undefined; updateUrl?: boolean | undefined }) {