Background
Surfaced by CodeRabbit during review of #335 (chat timeline windowing). Filed as a follow-up so #335 can land with the perf win while the issue stays tracked.
Problem
In `src/features/chat/runtime/projection.ts`, the windowed branch of `projectMessages` uses `countProjectedItemMessages(item)` (which assumes 1 ChatMsg per timeline item) to advance `totalMessages`, but `projectItem` can split a single timeline item into multiple ChatMsgs via `splitToolCallMessage` — e.g. an assistant message containing thinking, text, and tool_use blocks is emitted as separate entries.
```ts
// projection.ts ~L130-141
const messageCount = countProjectedItemMessages(item); // always 1 for user/assistant/thinking/etc.
totalMessages += messageCount;
if (messageCount > 0 && visibleMessages.length < visibleCount) {
const remaining = visibleCount - visibleMessages.length;
const projected = projectItem(item, options); // can be >1
visibleMessages.unshift(...projected.slice(-remaining));
}
```
Effect
- `totalMessages` undercounts when any item splits.
- `hasMore = totalMessages > visibleMessages.length` can incorrectly read `false` when there are actually more messages to load → "Show more" / "Load more" affordances disappear early.
- The slice assumption `projected.length ≤ messageCount` does not hold during splits.
Repro shape
A long session with at least one assistant turn whose message contains a mix of thinking/text/tool_use blocks, viewed with windowing enabled (the default after #335 lands).
Suggested fix
Two viable directions:
- Make `countProjectedItemMessages` split-aware — run the same split logic as `splitToolCallMessage` and count its output. Preserves the perf win (no full projection) but duplicates split logic.
- Refactor the windowed branch to project every item (use `projectItem().length` for the count) and only avoid the `unshift` cost for off-window items. Cleaner but partially negates the windowing perf benefit.
Option (1) is preferred; option (2) is a safer fallback.
Severity
Minor (per CodeRabbit). UX bug — no data loss, no crash — surfaces as a missing "load more" affordance in some long mixed-content sessions.
References
Background
Surfaced by CodeRabbit during review of #335 (chat timeline windowing). Filed as a follow-up so #335 can land with the perf win while the issue stays tracked.
Problem
In `src/features/chat/runtime/projection.ts`, the windowed branch of `projectMessages` uses `countProjectedItemMessages(item)` (which assumes 1 ChatMsg per timeline item) to advance `totalMessages`, but `projectItem` can split a single timeline item into multiple ChatMsgs via `splitToolCallMessage` — e.g. an assistant message containing thinking, text, and tool_use blocks is emitted as separate entries.
```ts
// projection.ts ~L130-141
const messageCount = countProjectedItemMessages(item); // always 1 for user/assistant/thinking/etc.
totalMessages += messageCount;
if (messageCount > 0 && visibleMessages.length < visibleCount) {
const remaining = visibleCount - visibleMessages.length;
const projected = projectItem(item, options); // can be >1
visibleMessages.unshift(...projected.slice(-remaining));
}
```
Effect
Repro shape
A long session with at least one assistant turn whose message contains a mix of thinking/text/tool_use blocks, viewed with windowing enabled (the default after #335 lands).
Suggested fix
Two viable directions:
Option (1) is preferred; option (2) is a safer fallback.
Severity
Minor (per CodeRabbit). UX bug — no data loss, no crash — surfaces as a missing "load more" affordance in some long mixed-content sessions.
References