docs/mobile-chat/9b-timeline/04-implementation-plan.md
Status: active · Task: 9b-timeline
Mobile chat renders assistant answers with no visibility into the agent's thinking/tool steps — the
AgentTimeline above each answer is a stub whose steps prop is always []. Web shows a full agent timeline
(a rail of collapsible reasoning/tool steps with a streaming "Thinking… (Ns)" header that auto-collapses to
"Thought for Ns · N steps"). 9b ports web's entire timeline shell 1:1 to mobile/ and wires the reasoning
step, so that: (a) thinking/tool activity is shown faithfully during and after streaming; (b) the composition
seam is a zero-refactor drop-in point for the tool renderers the owner will build immediately after
(search/fetch/python/custom-tool/deep-research/memory) and for 9c–9e. Backend, DB, and API are unchanged.
mobile/src/chat/messageProcessor.ts is a flat, cursor-incremental
reducer whose header comment states 9b extends it with turn/tab grouping; mobile/src/components/chat/AgentTimeline.tsx
already has the 36px rail + 24px avatar + reanimated shimmer + a TimelineStep primitive; MessageRow.AssistantMessage
already mounts the timeline above the answer and CitedSources (9a) below. 9b builds on all three.03-detailed-design.md
and .context/pr9b-deepread/*.md, extracted verbatim from web/src/app/app/message/messageComponents/**
(packetProcessor.ts, transformers.ts, interfaces.ts, renderMessageComponent.tsx, the timeline/hooks/*,
timeline/primitives/*, timeline/headers/*, ReasoningRenderer.tsx). Reasoning packets already stream from
backend/onyx/chat/llm_step.py.section_end synthesis is the correctness core. A step is marked complete almost entirely by
client-synthesized section_end — on a new turn_index (closes all prior groups) and on stop (closes all
open groups). The backend seldom sends it. Port injectSectionEnd/handleTurnTransition/handleStopPacket
verbatim (packetProcessor.ts:116-133, 190-208, 270-287).usePacketProcessor (web mutates a state
ref during render) → mobile useMemo full recompute (9a's proven, lint-safe pattern). usePacedTurnGroups (web
reads pacing refs during render) → useState written by an effect + timer handle in a ref (effect-only). Both
behavior-preserving (same grouped output, same 200 ms cadence). This is required by mobile's
react-hooks/refs lint; it is not a look/structure drift.FlatList or the answer markdown. Concretely:
the message row is React.memo'd, renderItem is useCallback'd with stable keys, the per-second timer is
isolated to the header subtree (not the whole AgentTimeline/row), and grouped/paced outputs never hand a fresh
object identity to a row that didn't change. (React Native official FlatList guidance; verified 2026.)useMemo full-recompute cost (acknowledged). The lint-safe usePacketProcessor reprocesses all packets per
flush (O(n)), vs web's incremental cursor. Fine at chat scale (hundreds of packets); a pathologically long
tool-heavy turn (thousands) doubles per-flush work. This is an internal implementation detail with no API impact,
so it can be re-optimized to incremental later without touching the seam — do not pre-optimize.PacketType enum now, per-tool interfaces later — so the engine/helpers compile once and never need an
enum edit; each future tool renderer adds only its obj interface + one findRenderer predicate wire-up.digestibleux.com, hatchworks
agent-ux, W3C accordion APG).circle, fold, expand, check-circle; stop-circle exists) and
confirming the mobile Button covers a tertiary icon button — per the web-parity principle, don't hand-roll a
divergent primitive.expandedText dead field dropped; no hover anywhere.Ordered, coherent changes. Each maps to a step Phase 5 bundles into ~500–700 LOC PRs.
mobile/src/chat/streamingModels.ts with the full web PacketType enum values and
the obj interfaces the engine/shell dereference (ReasoningStart/Delta/Done, TopLevelBranching,
ToolCallArgumentDelta + CODE_INTERPRETER_TOOL_TYPES, SearchToolStart.is_internet_search,
CustomToolStart.tool_name, ImageGenerationToolDelta.images, MessageStart.pre_answer_processing_seconds);
extend the ObjTypes union.mobile/src/chat/messageProcessor.ts into a faithful port of web packetProcessor.ts:
add the grouping fields to ProcessedMessageState + GroupedPacket; port getGroupKey, injectSectionEnd,
handleTurnTransition, handleStopPacket (grouping), handleStreamingStatusPacket,
handleToolAfterMessagePacket, categorization, buildGroupsFromKeys, hasContentPackets, and the packet-type
Sets. Preserve the 9a citation/document/isComplete behavior.mobile/src/chat/timeline/{transformers,packetUtils,packetHelpers,toolDisplay,reasoningState}.ts
ported verbatim (step→turn grouping + parallel detection; categorizers; per-family predicates + collapsed-streaming
sets; tool key/name/completion; reasoning heading extraction + delta accumulation).mobile/src/hooks/timeline/usePacketProcessor.ts (useMemo
recompute + derive toolTurnGroups/displayGroups/isComplete) and usePacedTurnGroups.ts (useState +
effect-driven 200 ms reveal, answer gating, history bypass; drop web's prevPacedRef).useTimelineUIState (7 states + booleans), useTimelineExpansion (auto-collapse +
userHasToggled), useTimelineHeader (text map), useStreamingDuration (live timer, backend-freeze),
useTimelineMetrics, useTimelineStepState (memory, dormant) under mobile/src/hooks/timeline/.renderers/timelineContract.ts (RenderType/RendererResult/MessageRenderer/
FullChatState/TimelineRendererResult) and renderers/findRenderer.ts (full 13-slot priority chain; chat +
reasoning wired, the rest tagged // PR 9x). Re-export from registry.ts.MessageTextRenderer.tsx to the render-prop MessageRenderer contract;
add RendererComponent.tsx (final-answer dispatch at FULL, mixed chat+image stubbed = 9e). Preserve 9a inline
citations + streamed markdown.circle, fold, expand, check-circle icons; confirm/port a
tertiary icon Button; add a muted/compact variant to StreamingMarkdown.tsx.components/chat/timeline/primitives/* (timelineTokens,
TimelineRoot/HeaderRow/Row/IconColumn/Surface/StepContent) with baked px tokens + dropped hover; add
StepContainer.tsx and TimelineRendererComponent.tsx (per-step expand + renderType derivation).renderers/ReasoningRenderer.tsx (constructReasoningState + extractFirstParagraph +
500 ms min-thinking gate) and timeline/ReasoningTextWindow.tsx (maxHeight markdown window). Register reasoning
in findRenderer.AgentTimeline.tsx into the shell (runs the state hooks + header
switch + body), add ExpandedTimelineContent.tsx, CollapsedStreamingContent.tsx, TimelineStep.tsx, the
headers/* (Streaming/Completed/Stopped; Parallel* dormant stubs), ParallelTimelineTabs.tsx (dormant,
linearized), toolIcons.ts, the Done/Stopped terminal step.MessageRow.AssistantMessage into the AgentMessage analog (run
usePacketProcessor + usePacedTurnGroups; AgentTimeline above; pacedDisplayGroups → RendererComponent
below; CitedSources last); fold/adjust usePacketDisplay to keep exposing processed for CitedSources.
Capture streamingStartedAt per assistant node in the PR-3 stream controller/store for the live timer.Primary type: RN Testing Library + Jest unit (the pure core + hooks carry essentially all the risk; there is no backend surface — reasoning packets already exist). Cover:
chat/timeline + messageProcessor): group key "{turn}-{tab}"; the three section_end
triggers (real packet, turn-transition closes prior groups, stop closes all open); tool-vs-display
categorization; finalAnswerComing + tool-after-message reset; hasContentPackets; model_index tolerance;
history-reload reset (array shrink).groupStepsByTurn parallel detection + turn/tab ordering.usePacedTurnGroups, fake timers): first step immediate, subsequent 200 ms apart, stop flush-all,
history bypass reveals instantly, answer withheld until pacing completes.useTimelineUIState all 7 states + each derived boolean; useTimelineExpansion auto-collapse
on answer/stop + userHasToggled suppression; useStreamingDuration per-second tick + backend-duration freeze.reasoningState heading extraction (markdown-heading rule, 60-char cap) + delta accumulation;
ReasoningRenderer 500 ms min-thinking gate (fake timers) + empty/pre-start branch.USER_CANCELLED stop shows the Stopped step.HARD device gate (owner-run, not automatable): on a dev build, drive a reasoning model and confirm the streaming shimmer + live timer, auto-collapse when the answer begins, tap-to-expand, the Done terminal step, and a hydrated (history-reloaded) render — plus that the migrated final-answer path + 9a Sources still render correctly.
Ran the mandatory 6-point challenge (web-verified checks 3 & 4).
Full-shell-now means each of the 6 deferred tool renderers + 9c/9e is a single new file + one findRenderer
wire-up — zero engine/enum/shell change; the grouping key already carries tab_index/sub_turn_index/model_index
for parallel/nested/multi-model. Sole caveat (documented, not a rewrite): the lint-safe usePacketProcessor is
O(n)/flush vs web's incremental cursor — fine at chat scale, re-optimizable later behind the same API.
Two brittle points, each with a concrete mitigation now in the plan: (a) usePacedTurnGroups (timers +
effect-published state) is the highest-bug-density file → fake-timer unit tests for first-immediate / 200 ms /
stop-flush / history-bypass; (b) the 1/sec timer + 200 ms reveals could churn the FlatList/answer → isolate the
timer to the header subtree, React.memo the row, stable keys, no per-tick identity churn (added as a hardening
note). section_end synthesis depends on turn-transition ordering but is ported verbatim + unit-tested.
Searched render-props-vs-hooks (2025), ref-during-render/purity, and RN FlatList streaming perf. (a) Render-props /
children-as-function remain the recognized standard for headless renderers where the wrapper owns the tree
(Downshift, React Aria, TanStack Table, Framer Motion AnimatePresence) — exactly the StepContainer↔renderer
split, so the render-prop contract is legitimate here, not legacy. (b) The ref restructure aligns with React's
own rule — react.dev's eslint-plugin-react-hooks/refs + purity docs say reading/writing ref.current during
render breaks purity/concurrent rendering; web's ref-during-render is the anti-pattern React now lints against, so
mobile's restructure is more correct, not a workaround. (c) FlatList streaming perf best practices (memo rows,
useCallback renderItem, stable keys, no per-tick object churn) confirmed and folded in.
Sources: react.dev refs lint,
react.dev purity,
patterns.dev render props,
RN FlatList optimization.
Claims verified: "progressive disclosure = industry default" (Phase 1 + re-verified); "refs-during-render must be restructured" (React official docs, above); "render-prop makes future ports mechanical" (valid for the headless- renderer case). Nuance stated plainly: render-props are not the modern default for brand-new logic-sharing (hooks are) — mobile adopts them purely for web-parity / mechanical future ports, an explicit, owner-chosen divergence from the hooks-default norm, not an oversight.
Mirrors web's exact timeline/ file tree (a dev who knows web finds the identical structure), pure logic isolated +
unit-tested, clear engine/hooks/primitives/renderers boundaries. The ~35-files-for-one-wired-renderer surface only
pays off if the follow-up renderers get built — which the owner has explicitly committed to doing immediately,
so the investment is justified rather than speculative. Gotcha to guard with a comment: the render-prop inversion
(renderer calls children(results), never returns its own tree) — documented in 03 §9.
The render-prop migration of shipped PR-3/9a code is a root-cause fix — it unifies mobile onto web's contract
now so there is no later refactor; the alternative (bolt reasoning onto the simpler {matches,Component}
contract) is precisely the "refactor later" the owner forbade. The two hook restructures are fixes (align with
React's purity rule), not lint-suppression. The scoped deferrals (search sub-labels, parallel tabs, reasoning
auto-scroll) are documented scope boundaries restored by later phases, not symptom-patches. No patch-vs-fix
decision to surface.
Verdict: all six pass (2 concerns hardened in-plan). No patch-vs-fix escalation. Cleared for Phase 5.