mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
feat(mobile): agent timeline composition + parallel tabs (#13695)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45b92975cc
commit
b345a930ed
40 files changed
+2397
-255
No files matched your search
@@ -198,6 +198,12 @@ that changes the timeline on screen.
|
||||
- **Feature-flag state:** N/A — **the timeline goes live.**
|
||||
- **Tests on merge:** RN Testing Library — mocked reasoning packet stream renders the streaming header → step → auto-collapse → tap-expand → Done; Stopped path. **HARD device gate (owner):** dev build, reasoning model — streaming shimmer + live timer, auto-collapse on answer, tap-to-expand, Done step, hydrated render; migrated answer + 9a Sources intact; streaming re-render stays smooth (perf-isolation from `04`).
|
||||
- **Drift checkpoint:** Re-confirm the `unify-chat-input` `ChatSurface` composition (post-PR6 refactor) is where `MessageRow`/`AgentTimeline` mount, and that per-conversation draft/reset rules don't interfere with the per-node timeline state.
|
||||
- **As built (2026-08-03, "parallel tabs pulled forward" — owner-gated before coding):** Four `AskUserQuestion` gates. The big one: the owner checked whether parallel tabs were planned anywhere under `docs/mobile-chat/` — they were not (only the unscheduled "After 9b" line below) — and ruled **build them now**, so **`ParallelTimelineTabs` and `ParallelStreamingHeader` ship LIVE, not dormant**, and the linearize divergence (`03` §8(d)) is **retired**. That required a new mobile primitive: **`components/ui/tabs.tsx`**, an RN port of Opal's `pill` `Tabs` (Root context + `List` + `Trigger`), which the owner approved over hand-rolling a lookalike. Other gates: port `stop-circle` 1:1 from Opal (the 9b.5 icon precedent); **fold `usePacketDisplay` into `usePacketProcessor`** (expose `processed`, delete the hook + its test) rather than reduce every packet twice per flush; ship it all as one PR. New: `components/chat/timeline/{ExpandedTimelineContent,CollapsedStreamingContent,TimelineStep,TimelineStepComposer,ParallelTimelineTabs,ShimmerText}.tsx` + `toolIcons.ts`, `timeline/headers/{Streaming,Completed,Stopped,ParallelStreaming}Header.tsx`, `timeline/primitives/TimelineTopSpacer.tsx`, `components/ui/tabs.tsx`, and 6 1:1 Opal icon ports (`stop-circle`, `branch`, `user`, `code`, `book-open`, `slow-time`). Modified: `AgentTimeline.tsx` (stub → full shell), `MessageRow.tsx` (→ the AgentMessage analog), `hooks/timeline/usePacketProcessor.ts` (+`processed`), `chat/interfaces.ts` + `chat/chatHistory.ts` (+`streamingStartedAt`, +`processingDurationSeconds`), `hooks/useChatController.ts` + `hooks/useChatSessionController.ts` (the two stream-start stamps), `lib/time.ts` (+`formatDurationSeconds`, ported from Opal `time.ts`). **~1480 LOC** — roughly double the 500–700 band, which the owner accepted when folding tabs in.
|
||||
- **Divergences from web (9b.7's own, on top of `03` §8):** (a) **no hover anywhere** — web's `headerIsInteractive` only toggled a hover class, so it has no mobile analog and each header owns its own `Pressable` instead; (b) `useTimelineStepState` is **not called** by the shell (its only web consumers are the memory tag and that hover flag) and `CompletedHeader` **drops web's memory tag/tooltip/`MemoriesModal`** — the memory renderer is a later phase; (c) shimmer = an **opacity pulse** (`ShimmerText`, RN `Animated` so the timeline stays jest-renderable), not web's `background-clip:text` gradient; (d) the expanded body has **no entrance animation** (web's `animate-in fade-in slide-in-from-top-2`) — a 300 ms flourish isn't worth putting reanimated back on the test path; (e) `streamingStartTime` is a **per-node prop**, not web's per-session store read, and a **resumed** run restarts the timer from the resume (this client never observed the run's true start; web shows no timer at all there); (f) the ported `Tabs` ships the **`pill` variant only** (no `contained`/`underline` — no consumer) and **drops the scroll arrows** (a touch list is swiped, not arrow-stepped); (g) `TimelineStep` is split into its own module — web keeps it private inside `ExpandedTimelineContent`, which on mobile would cycle `ExpandedTimelineContent → ParallelTimelineTabs → TimelineStep`; (h) `getToolIcon` uses Onyx glyphs where web falls back to `react-icons` against its own icon rule (`FiTool` → `cpu`, matching `chat/tools.ts`'s custom-tool fallback; `FiList` → `text-lines-small`).
|
||||
- **Also fixed here:** hydrated turns carried no duration at all, so **every** reopened chat's header would have read "Thought for some time" — `processing_duration_seconds` (already on the backend response, `models.py:232`) is now mapped through `chatHistory`.
|
||||
- **Adversarial review (4 lenses × 3 refute-first skeptics) — 8 raised, 6 fixed:** **(1) HIGH — the Stopped path was unreachable in production.** A user stop aborts the reader *before* the backend's `stop` packet arrives, so `stopPacketSeen` never flipped: the turn stayed in its STREAMING ui-state (and `useStreamingDuration`'s 250 ms interval kept ticking) until the session was reloaded. Fixed by synthesizing a `USER_CANCELLED` stop packet in `runChatStream`'s `finally` on abort (`buildUserCancelledStopPacket`); the resume path already self-heals, since it re-hydrates from a snapshot the backend replays an `OverallStop` into. **(2) MEDIUM — FlashList recycles React keys**: `RenderStackManager.syncItem` hands a pooled key to a different `stableId`, so a scrolled-away turn's component state (timeline expansion, active branch tab, open sources sheet) was inherited by an unrelated message — and since `userHasToggled` latches, that turn could never auto-collapse. `keyExtractor` only supplies the stableId, not the React key; fixed by keying `MessageRow` on `nodeId` inside `renderItem`. **(3) LOW ×3** — the `chatState` memo's count proxies could pin a stale map on mobile (web gets away with it by mutating in place), the Tabs baseline painted over the sliding indicator, and Opal's `pt-1` belongs to its no-scroll-arrows branch. **(4)** 5 files weren't prettier-formatted. Accepted as-is: the `CitedSources` gate keeps 9a's `processed.isComplete` (a briefly-early Sources bar during answer retraction is not worth changing shipped 9a semantics here).
|
||||
- **Verified:** `tsc` clean, lint clean (2 pre-existing `_layout` warnings), prettier clean, **596 jest pass** across 76 suites (+40 new: the mandated composition smoke test — reasoning stream → "Thinking" step → auto-collapse to "Thought for Xs · 1 step" → tap-expand → Done; USER_CANCELLED → Interrupted Thinking → Stopped; parallel turn → a tab per branch in both the streaming header and the expanded body, incl. switching branches — plus the `Tabs` primitive, `StreamingHeader` timer/freeze under fake timers, `getToolIcon`, `formatDurationSeconds`, the `chatHistory` duration mapping, the `streamingStartedAt` stamp, and the synthetic-stop-on-abort regression).
|
||||
- **HARD DEVICE GATE STILL OWED (owner-run):** a dev build against a reasoning model — streaming shimmer + live timer, auto-collapse when the answer starts, tap-to-expand, the Done step, a hydrated (reopened) render showing a real "Thought for Xs", the migrated answer + 9a Sources intact, and that streaming re-render stays smooth. Parallel tabs additionally need a **multi-branch run** to see the pill strip and the sliding indicator on device.
|
||||
|
||||
## After 9b — the follow-up renderer phases (not designed here)
|
||||
|
||||
|
||||
Reference in new issue
Block a user