Skip to content

fix(app): load older history when chat is shorter than viewport#1294

Open
kongjiadongyuan wants to merge 2 commits into
getpaseo:mainfrom
kongjiadongyuan:codex/fix-short-history-autoload
Open

fix(app): load older history when chat is shorter than viewport#1294
kongjiadongyuan wants to merge 2 commits into
getpaseo:mainfrom
kongjiadongyuan:codex/fix-short-history-autoload

Conversation

@kongjiadongyuan
Copy link
Copy Markdown
Contributor

@kongjiadongyuan kongjiadongyuan commented Jun 2, 2026

Summary

  • load older agent history after viewport/content measurement, not only after user scroll
  • share the history-start scroll math across forward web and inverted native stream strategies
  • cover short-content cases with pure strategy tests and a web regression test that triggers without a scroll event

Why

When the initial chat content is shorter than the viewport, there is no scrollable top edge. The existing lazy history loader only ran from scroll handlers, so users could get stuck with a short partial timeline even though hasOlderHistory was true.

Tests

  • npm run test --workspace=@getpaseo/app -- src/agent-stream/render-strategy.test.ts src/agent-stream/strategy-web.test.tsx --bail=1
  • npm run lint -- packages/app/src/agent-stream/strategy.ts packages/app/src/agent-stream/strategy-native.tsx packages/app/src/agent-stream/strategy-web.tsx packages/app/src/agent-stream/render-strategy.test.ts packages/app/src/agent-stream/strategy-web.test.tsx
  • npm run typecheck --workspace=@getpaseo/app
  • pre-commit: lint, format check, full workspace typecheck

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR fixes a bug where chats with content shorter than the viewport could never trigger older history loads, because the lazy loader was wired only to scroll events (which never fire when content can't scroll). The fix calls a new maybeLoadOlderHistory helper from ResizeObserver, layout-change, and content-size-change callbacks in addition to scroll events, so the history check runs after every measurement regardless of scrollability.

  • strategy.ts gains isNearHistoryStartForForwardStream, isNearHistoryStartForInvertedStream, and the dispatch wrapper isNearHistoryStartForStreamRenderStrategy, consolidating the "near old edge" math that was previously inlined (and divergent) in the native scroll handler.
  • Both strategy-web.tsx and strategy-native.tsx replace inline proximity checks with calls to the new shared helpers and add a useEffect that re-evaluates when hasOlderHistory or isLoadingOlderHistory changes.
  • New pure-strategy unit tests cover the short-content path for both forward and inverted strategies; a JSDOM regression test verifies the ResizeObserver wiring.

Confidence Score: 5/5

Safe to merge — the change is well-scoped, the new proximity math is unit-tested for both forward and inverted strategies, and the existing isLoadingOlderHistory guard prevents duplicate history fetches.

The fix correctly identifies that scroll-only triggering misses short-content cases, extracts the proximity check into a shared helper, and wires it to measurement events. The guard chain prevents premature or repeated calls. Pure-strategy tests cover both strategy types and the edge case being fixed. No regressions are visible in adjacent scroll/bottom-anchor logic.

No files require special attention. strategy-web.test.tsx uses a JSDOM component-mounting pattern that was already flagged in a prior review thread; all other files are straightforward.

Important Files Changed

Filename Overview
packages/app/src/agent-stream/strategy.ts Adds StreamNearHistoryStartInput type alias and three new exported functions for near history start detection; logic is straightforward and well-tested.
packages/app/src/agent-stream/strategy-web.tsx Extracts maybeLoadOlderHistory into a useStableEvent and calls it from the ResizeObserver, the agentId-reset rAF, and a new effect on hasOlderHistory/isLoadingOlderHistory; offset calculation correctly handles short-content and follow-output modes.
packages/app/src/agent-stream/strategy-native.tsx Mirrors the web-side changes: maybeLoadOlderHistory replaces inline distance math in the scroll handler and is also called from handleListLayout, handleContentSizeChange, the agentId rAF, and the new hasOlderHistory/isLoadingOlderHistory effect.
packages/app/src/agent-stream/render-strategy.test.ts Adds three pure-strategy tests for isNearHistoryStartForStreamRenderStrategy covering short-content true path for both forward and inverted strategies, and the negative (long content) path.
packages/app/src/agent-stream/strategy-web.test.tsx Adds a JSDOM/createRoot component-mounting test verifying the ResizeObserver wiring; the test pattern was already flagged in a previous review thread for this file.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Component Mount / agentId change] --> B[requestAnimationFrame]
    B --> C[historyStartReadyRef = true]
    C --> D[maybeLoadOlderHistory]
    E[ResizeObserver fires] --> F[updateScrollMetrics]
    F --> D
    G[handleScroll / handleListLayout / handleContentSizeChange] --> D
    H[hasOlderHistory or isLoadingOlderHistory changes] --> D
    D --> I{Guards pass?}
    I -- No --> J[return early]
    I -- Yes --> K{followOutput?}
    K -- true --> L[offsetY = max 0 scrollHeight minus clientHeight]
    K -- false --> M[offsetY = scrollTop]
    L --> N{isNearHistoryStart?}
    M --> N
    N -- No --> O[return]
    N -- Yes --> P[onNearHistoryStart called]
    P --> Q[isLoadingOlderHistory blocks further calls]
Loading

Reviews (2): Last reviewed commit: "chore(app): reuse stream scroll input ty..." | Re-trigger Greptile

Comment thread packages/app/src/agent-stream/strategy.ts Outdated
Comment on lines 211 to 274
expect(onNearHistoryStart).toHaveBeenCalledTimes(1);
});

it("fires near-history-start after measurement when content is too short to scroll", async () => {
const strategy = createWebStreamStrategy({ isMobileBreakpoint: true });
const viewportRef = React.createRef<StreamViewportHandle>();
const onNearHistoryStart = vi.fn();
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);

act(() => {
root?.render(
<>
{strategy.render({
agentId: "agent",
segments: {
historyVirtualized: [],
historyMounted: [userMessage(1), userMessage(2)],
liveHead: [],
},
boundary: {
hasVirtualizedHistory: false,
hasMountedHistory: true,
hasLiveHead: false,
},
renderers: createRenderers(vi.fn()),
listEmptyComponent: null,
viewportRef,
routeBottomAnchorRequest: null,
isAuthoritativeHistoryReady: true,
onNearBottomChange: vi.fn(),
onNearHistoryStart,
isLoadingOlderHistory: false,
hasOlderHistory: true,
scrollEnabled: true,
listStyle: null,
baseListContentContainerStyle: null,
forwardListContentContainerStyle: null,
})}
</>,
);
});

const scrollContainer = container.querySelector('[data-testid="agent-chat-scroll"]');
expect(scrollContainer).toBeInstanceOf(HTMLElement);

await act(async () => {
await new Promise((resolve) => requestAnimationFrame(resolve));
});
expect(onNearHistoryStart).toHaveBeenCalledTimes(0);

Object.defineProperty(scrollContainer, "clientHeight", { configurable: true, value: 400 });
Object.defineProperty(scrollContainer, "scrollHeight", { configurable: true, value: 200 });
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 0 });
act(() => {
for (const callback of resizeObserverCallbacks) {
callback([], {} as ResizeObserver);
}
});

expect(onNearHistoryStart).toHaveBeenCalledTimes(1);
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 JSDOM component-mounting test for logic that lives in maybeLoadOlderHistory

The project style guide explicitly flags JSDOM component-mounting tests as a banned pattern ("React components are not the unit — extract the logic and unit-test the extraction; cover component behavior through app E2E with a real harness"). The onNearHistoryStart firing condition under short content is pure scroll-math already covered by the new pure-strategy tests in render-strategy.test.ts. The part that isn't covered — that maybeLoadOlderHistory is wired to the ResizeObserver and fires after the historyStartReadyRef rAF gate — is precisely the kind of integration that belongs in an E2E test, not a JSDOM simulation. Consider whether this test adds confidence beyond render-strategy.test.ts or whether the ResizeObserver wiring should be verified in E2E.

Rule Used: # Code Review Pattern Reference: Slop, Tests, Feat... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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