Skip to content

fix(app-builder): show newest V2 messages first with load-earlier option#4463

Open
wombatepiclandingstudio wants to merge 6 commits into
Kilo-Org:mainfrom
wombatepiclandingstudio:fix/app-builder-v2-show-newest-first
Open

fix(app-builder): show newest V2 messages first with load-earlier option#4463
wombatepiclandingstudio wants to merge 6 commits into
Kilo-Org:mainfrom
wombatepiclandingstudio:fix/app-builder-v2-show-newest-first

Conversation

@wombatepiclandingstudio

@wombatepiclandingstudio wombatepiclandingstudio commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

App Builder V2 sessions currently render every message in chronological order (oldest → newest) and rely on auto-scroll to reach the bottom. On a long conversation, this means a slow initial render and a jarring jump. This change makes V2 sessions render only the tail of the conversation (from the last user turn onwards) and adds a Load earlier messages button at the top that reveals one older user turn per click.

  • V1 sessions are untouched (they already paginate by user-session via paginateMessages and show the same button).
  • No backend, tRPC, or store changes — the V2 session store already holds every message in memory; the change is purely a render-side slice in V2SessionMessages.
  • The visible window is local component state, re-anchored to the current tail whenever a new user turn is appended (including the optimistic user message on send), and clamped to the current message length so a sudden shrink never overflows.
  • Auto-scroll to bottom and the existing shouldAutoScroll / "Scroll to bottom" behavior continue to work because the visible slice always includes the tail where new streaming messages arrive.

Follow-up fix in commit e841fc978 — re-anchor on new user turn:

The first version of the effect clamped visibleCount downward when sessionState.messages shrank, and re-ran tailFromLastUserTurn only when session.info.id changed (i.e. session switch). It did nothing when the message list grew with a new user message, so after the initial turn completed and visibleCount settled at e.g. 2, sending a new message would append the optimistic user message and messages.slice(-visibleCount) would show a stale mix of the previous turn's tail plus the new user message until the assistant reply happened to push the fixed-size window past the boundary.

The fix tracks the id of the tail-most user message in a ref and re-anchors the window to the new tail whenever that id changes (which only happens when a brand-new user message is appended at the tail; assistant streaming never changes it). The shrink clamp is preserved via a functional setter.

New helpers in apps/web/src/components/app-builder/utils/filterMessages.ts:

  • tailFromLastUserTurn(messages, isUser?) — returns the slice from the last user-role message to the end, or the full list if no user message exists.
  • loadEarlierUserTurn(messages, currentVisibleCount, isUser?) — grows the visible window backwards by exactly one user turn.
  • lastUserIndex(messages, isUser) — exported for the component to detect when the tail-most user message changes.

Both pagination helpers are generic over the message type via a role-checker callback (defaults to getMessageRole for V1 CloudMessage[]), and accept a V2 StoredMessage[] via a msg => msg.info.role callback at the call site.

Verification

Manual only — automated checks were run but the full test suite requires a live Postgres which is not available in this sandbox.

  • pnpm typecheck in apps/web — passes.
  • pnpm lint on the changed files — 0 warnings, 0 errors.
  • pnpm format — clean.
  • pnpm test -- src/components/app-builder/utils/filterMessages.test.ts — cannot run here (sandbox lacks Postgres for the web app's global Jest setup). The unit tests cover: tailFromLastUserTurn (no user / first / middle / re-anchor to newly appended user turn / user_feedback), loadEarlierUserTurn (no-op / grows one turn / fallback to full), lastUserIndex (none / middle / user_feedback), getMessageRole, and a paginateMessages V1 regression check.

UX smoke test (to be performed by reviewer locally):

  1. Open an App Builder project with a long V2 history → confirm the latest user turn and its (possibly in-progress) assistant response are visible, scroll is at the bottom, and a "Load earlier messages" button appears at the top.
  2. Click the button repeatedly → confirm one older user turn is revealed each click, and the button disappears once the full history is visible.
  3. Send a new message → confirm the visible window re-anchors to just the new user turn immediately (not the previous turn's tail + the new user message), and the auto-scroll jumps to the new bottom. This is the behavior the follow-up fix targets.
  4. Continue streaming → confirm the window stays anchored to the new turn as the assistant reply arrives.
  5. Scroll up manually while streaming → confirm auto-scroll correctly disengages and re-engages (regression check only — existing behavior).

Visual Changes

N/A — no visual change to the message bubbles or layout. The only new element is a "Load earlier messages" button at the top of V2 sessions, which uses the same styling as the existing V1 button (Button variant="ghost" size="sm").

Reviewer Notes

  • V2SessionMessages now uses useState / useEffect / useCallback / useRef to manage the visible window; all four are already imported at the top of the file.
  • The single re-anchor effect depends on [sessionState.messages, getRole] (not visibleCount), so it re-runs on every messages change but does not loop on its own setters: the functional-setter shrink path returns prev unchanged when in range, which React bails out of. The new-user-message path returns early after setting visibleCount.
  • splitByContiguousPrefix static/dynamic split runs on the visible slice rather than the full message list, so the streaming boundary is always computed within what's actually shown.
  • childSessionMessages map is unaffected — child data is keyed by child session id and only rendered when a visible parent message references it.
  • The session.info.id re-anchor effect from the first version was removed; the ref-based effect handles both intra-session new-turn and inter-session re-mounting (the component is key={session.info.id} keyed, so a session switch already remounts it).

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
}, [session.info.id]);

useEffect(() => {
setVisibleCount(prev => Math.min(prev, sessionState.messages.length));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: visibleCount only ever shrinks (or resets on session-id change) — it never grows to re-anchor onto a newly-started user turn.

This effect clamps visibleCount down when sessionState.messages.length decreases, but does nothing when it increases (Math.min(prev, newLength) is a no-op for growth). tailFromLastUserTurn is only re-run in the session.info.id effect above, which fires on session switch, not when a new user message is appended to the same session.

Concretely: after the initial turn completes, visibleCount settles at that turn's message count (e.g. 2). When the user sends a new message, the optimistic user message is appended (messages.length +1) before the assistant's reply message exists yet. Because visibleCount wasn't re-derived, sessionState.messages.slice(-visibleCount) now shows a stale mix of the tail-end of the previous turn together with the brand-new user message, instead of just the new turn, until the assistant's message is created and the fixed-size window naturally catches up. This directly undermines the PR's stated goal ("the latest user turn and its ... assistant response are visible").

Consider re-deriving visibleCount via tailFromLastUserTurn whenever a new user-role message appears at the tail (not just clamping downward on shrink), so the window re-anchors to the new turn immediately rather than self-correcting only once the reply arrives.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The follow-up commit fixes the prior CRITICAL finding exactly as suggested: visibleCount now grows via Math.max(prev, messages.length - lastIdx) while streaming within the same user turn, so the user's message and earlier same-turn chunks no longer drop out of view; no new issues were introduced by this small diff.

Files Reviewed (1 file)
  • apps/web/src/components/app-builder/AppBuilderChat.tsx
Previous Review Summaries (2 snapshots, latest commit d8748c7)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit d8748c7)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The follow-up re-anchor fix correctly resolves the prior stale-mix issue on new user turns, but the visible tail window still never grows during a turn's own streaming, so the user's message (and earlier same-turn chunks) drop out of view as soon as the assistant appends more messages.

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/web/src/components/app-builder/AppBuilderChat.tsx 407 visibleCount is reset to 1 on a new user turn but never grows as the assistant streams more messages into that same turn, so slice(-visibleCount) drops the user's own message and earlier same-turn chunks out of view
Files Reviewed (3 files)
  • apps/web/src/components/app-builder/AppBuilderChat.tsx - 1 issue
  • apps/web/src/components/app-builder/utils/filterMessages.ts - 0 issues
  • apps/web/src/components/app-builder/utils/filterMessages.test.ts - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit 87b606d)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The new V2 tail-pagination window in AppBuilderChat.tsx only shrinks on message-array shrink, never re-anchors when a new turn starts, causing a brief stale render mixing the previous turn's tail with the new user message.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/components/app-builder/AppBuilderChat.tsx 393 visibleCount never grows to re-anchor on a new user turn, only shrinks on array shrink or resets on session-id change
Files Reviewed (3 files)
  • apps/web/src/components/app-builder/AppBuilderChat.tsx - 1 issue
  • apps/web/src/components/app-builder/utils/filterMessages.ts - 0 issues
  • apps/web/src/components/app-builder/utils/filterMessages.test.ts - 0 issues

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-5 · Input: 48 · Output: 11.2K · Cached: 1.4M

Review guidance: REVIEW.md from base branch main

The previous effect clamped visibleCount downward when messages shrank but
never re-derived it on growth, so the window's left edge stayed fixed at
the previous turn's boundary. When the user sent a new message, the
optimistic user message was appended to the message list but
messages.slice(-visibleCount) still showed a stale mix of the previous
turn's tail plus the new user message until the assistant reply happened
to push the fixed-size window past the boundary.

Track the id of the tail-most user message in a ref and re-anchor the
window (visibleCount = messages.length - lastUserIdx) whenever that id
changes, which only happens when a new user turn is appended. Assistant
streaming does not change the last-user-message id, so the window
correctly stays anchored to the current turn as the reply streams in.

The shrink clamp is preserved via a functional setter so a sudden shrink
of the message list cannot leave visibleCount pointing past the end.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
setVisibleCount(messages.length - lastIdx);
return;
}
setVisibleCount(prev => (prev > messages.length ? messages.length : prev));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Visible window never grows during streaming, so the current turn's earlier messages (including the user's own prompt) disappear as soon as more messages are appended

visibleMessages = sessionState.messages.slice(-visibleCount) is a fixed-size sliding window. On a new user turn, visibleCount is reset to exactly messages.length - lastIdx (typically 1, just the new user message) — but once the assistant reply starts streaming and appends additional message entries (thinking/tool-call/text chunks, per splitByContiguousPrefix's static/dynamic split), this branch is not taken again for the same turn (the tail-most user id hasn't changed), so execution falls through to the clamp-only branch here, which never grows visibleCount. With visibleCount frozen at 1, slice(-1) now returns only the newest appended message, dropping the user's own message (and any earlier assistant chunks of the same turn) out of view for the rest of the turn.

This is a regression versus the pre-fix behavior: previously the (buggy) frozen visibleCount at least inherited a larger value from the prior turn, giving some accidental buffer before this manifested; now it's reliably set to 1 right at the turn boundary, so the user's own message vanishes on the very first assistant-streamed message of essentially every turn. This directly undermines the PR's stated goal of keeping "the latest user turn and its ... assistant response" visible.

Suggested change
setVisibleCount(prev => (prev > messages.length ? messages.length : prev));
setVisibleCount(prev => Math.min(messages.length, Math.max(prev, messages.length - lastIdx)));

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

The previous effect clamped visibleCount downward but never grew it, so
on a turn boundary visibleCount was reset to messages.length - lastIdx
(typically 1, just the new user message). When the assistant reply then
streamed additional message entries (thinking/tool-call/text chunks) at
the tail, visibleCount stayed frozen at 1 and slice(-1) returned only
the newest chunk, dropping the user's own prompt and earlier chunks of
the current turn out of view.

Clamp visibleCount to [messages.length - lastIdx, messages.length]
during streaming so the window grows with the tail as new chunks
arrive, while still preserving any prior Load-earlier expansion and
resetting to the new turn's natural tail when a new user message lands.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
@wombatepiclandingstudio wombatepiclandingstudio force-pushed the fix/app-builder-v2-show-newest-first branch from d8748c7 to 430b1dd Compare July 9, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant