首页
/ Onyx 移动端 Agent 推理时间轴(Agentic Reasoning Timeline):从 9b.1 到 9b.7 的七 PR 分层落地路线图

Onyx 移动端 Agent 推理时间轴(Agentic Reasoning Timeline):从 9b.1 到 9b.7 的七 PR 分层落地路线图

2026-09-09 18:36:33作者:宣利权Counsellor

本文基于 docs/mobile-chat/9b-timeline/05-pr-roadmap.md 展开。该文档是 Onyx(danswer 仓库,开源 AI 平台)移动端 mobile-chat 子阶段 9b 的 PR 交付路线图:把 Web 端「Agent 推理时间轴」(agent timeline)外壳 1:1 移植到 React Native + Expo 移动应用,并按 7 个可独立合并的 PR 分层落地。读完本文,你将掌握这套 3.7k–4.3k LOC 的移植工作如何被切分成 9b.1 引擎 → … → 9b.7 组合点亮的序列,理解每个 PR 的目标、文件清单、测试与漂移检查点,以及 9b.7 上线时与 Web 实现的全部有意分歧。

一、背景:为什么一个功能要拆成 7 个 PR

Web 端在助手回答上方展示了一条完整的 agent timeline:一条垂直的步骤轨道(rail),包含推理 "Thinking" 步骤、搜索/工具步骤,每个步骤有图标、状态头、连接线和可折叠/展开的正文;流式期间显示闪烁的 "Thinking… (12s)" 头,答案一旦开始便自动折叠成 "Thought for 12s · 3 steps" 胶囊。而移动端聊天目前对助手的思考/工具过程没有任何可见性——AgentTimeline 只是一个 steps prop 恒为 [] 的桩(stub)。

把 Web 的时间轴外壳"忠实移植"(Approach C——Faithful Shell First,Owner 明确要求 "match web, no refactor later, want everything")是一块约 3.7–4.3k LOC 的工作,因此被切片为 7 个分层、可独立合并的 PR。整个系列的发布策略是 pre-production → 无功能开关

  • 各分层以 "dark"(暗态) 方式落地:编译通过 + 单元测试通过,但尚未挂到屏幕上
  • 直到组合 PR 9b.7 才把时间轴"点亮"(lights up);
  • 序列中唯一的中途行为变更是 9b.4(最终答案路径迁移到共享契约,行为保持一致);
  • 每个 PR 都必须让 main 保持可构建,tsc / lint / jest 全绿,应用保持可用。

该子阶段的完整上下文见 docs/mobile-chat/9b-timeline/00-index.md(需求与研究)、docs/mobile-chat/9b-timeline/02-high-level-design.md(端到端设计与决策)、docs/mobile-chat/9b-timeline/03-detailed-design.md(精确契约与逐文件职责)以及 docs/mobile-chat/9b-timeline/04-implementation-plan.md(实现计划与计划挑战结果)。

二、总体概览:7 个 PR 一览与依赖序列

PR 标题 预估 LOC 依赖 关键交付物
9b.1 feat(mobile): timeline grouping engine + packet contracts ~700 纯分组 reducer(turn/tab + section_end 合成)+ 完整 PacketType 枚举 + 纯步骤 helper;单元测试覆盖。Dark。
9b.2 feat(mobile): timeline processor + pacing hooks ~590 9b.1 usePacketProcessor(lint 安全重算)+ usePacedTurnGroups(200ms 揭示),两个重构后的 hooks;单元测试覆盖。Dark。
9b.3 feat(mobile): timeline state hooks ~550 9b.1 useTimelineUIState / Expansion / Header / StreamingDuration / Metrics / StepState;单元测试覆盖。Dark。
9b.4 feat(mobile): render-prop renderer contract + answer migration ~510 9b.1 RendererResult / MessageRenderer 契约 + findRenderer + RendererComponent;将最终答案 MessageTextRenderer 迁移到该契约。Live(答案路径)。
9b.5 feat(mobile): timeline primitives + StepContainer ~650 9b.4 轨道/表面/内容原语 + StepContainer + TimelineRendererComponent + 新图标/Button(ASK)+ StreamingMarkdown muted 变体。Dark。
9b.6 feat(mobile): reasoning renderer ~450 9b.4, 9b.5 ReasoningRenderer + reasoningState + ReasoningTextWindow;注册进 findRenderer;单元测试覆盖。Dark。
9b.7 feat(mobile): agent timeline composition ~700 9b.2, 9b.3, 9b.5, 9b.6 AgentTimeline 外壳 + 头部 + Expanded/Collapsed 内容 + MessageRow 接线 + streamingStartedAt点亮时间轴。设备门禁。

依赖序列如下:

9b.1 engine+contracts+helpers ─┬─► 9b.2 processor+pacing hooks ──┐
   (dark, unit-tested)         ├─► 9b.3 state hooks ─────────────┤
                               └─► 9b.4 contract + answer path ──┼─► 9b.5 primitives+StepContainer ─► 9b.6 reasoning ─┐
                                        (live: answer)           │        (dark)                        (dark)        │
                                                                 └──────────────────────────────────────────────────►┴─► 9b.7 composition
                                                                                                                          (LIGHTS UP + device gate)

要点:9b.1 是根,9b.2 / 9b.3 / 9b.4 从它扇出(可并行);9b.5→9b.6 挂在共享契约之上;9b.7 依赖 hooks(9b.2/9b.3)、UI 原语(9b.5)和推理渲染器(9b.6)——它是唯一把时间轴真正画上屏的 PR。

三、PR 9b.1:分组引擎 + 报文契约 + 纯 helper(Dark,单元测试)

  • 目标:纯的、可单测的数据核心——把报文(packets)分组为与 Web 完全一致的时间轴步骤。
  • 范围(in):完整的 Web PacketType 枚举 + 引擎/外壳 obj 接口(见 03-detailed-design.md §1);把 messageProcessor 扩展成忠实的 packetProcessor 移植(分组映射、turn 切换 + stop 时的 section_end 合成、工具/展示分类、finalAnswerComingbuildGroupsFromKeyshasContentPackets);纯步骤 helper。
  • 范围外(out):所有 hooks、所有 UI、推理渲染器。没有任何东西渲染得不同。
  • 文件
文件 新建/修改 切片
mobile/src/chat/streamingModels.ts 修改 枚举值 + reasoning/branch/tool-arg 接口 + ObjTypes
mobile/src/chat/messageProcessor.ts 修改 分组字段 + section_end 合成 + 分类(保留 9a citations/docs)
mobile/src/chat/timeline/transformers.ts 新建 TransformedStep / TurnGroup / groupStepsByTurn
mobile/src/chat/timeline/packetUtils.ts 新建 isToolPacket / isDisplayPacket / getTextContent / …
mobile/src/chat/timeline/packetHelpers.ts 新建 各报文族谓词 + 折叠流式集合
mobile/src/chat/timeline/toolDisplay.ts 新建 parseToolKey / getToolName / isToolComplete / hasToolError
mobile/src/chat/timeline/__tests__/* 新建 分组键、3 个 section_end 触发、分类、并行检测、model_index 容忍、历史重置
  • 预估规模:~700 LOC(引擎 + helper + 测试是紧密耦合的一个整体,把 helper 拆开会导致引擎无法编译)。
  • 依赖:无。功能开关状态:N/A(上线前;纯增量;dark)。
  • 合并时测试:Jest 单元——分组 + section_end 合成 + transformers 全量覆盖;messageProcessor 仍通过 9a 的 citation/document 测试。
  • 漂移检查点:确认后端推理报文形状(reasoning_start/delta/done)与 Placement 字段仍与 03/deepread 记录一致;重新核对 web/.../packetProcessor.ts 自提取以来未变。

源码佐证:契约与引擎的真实形态

这部分规划在仓库中已有相当基础。mobile/src/chat/streamingModels.tsPacketType 枚举已包含 REASONING_START = "reasoning_start"REASONING_DELTA = "reasoning_delta"REASONING_DONE = "reasoning_done"TOP_LEVEL_BRANCHING = "top_level_branching" 等全部 Web 线类型(streamingModels.ts),并定义了 ReasoningStart / ReasoningDelta(携带 reasoning: string)/ ReasoningDone 接口,以及 Placement 的全部四个字段(turn_index / tab_index / sub_turn_index / model_index,见 streamingModels.ts)——这正是分组键 "{turn_index}-{tab_index}" 的数据基础。CODE_INTERPRETER_TOOL_TYPES = { PYTHON: "python" } 也已存在。

mobile/src/chat/messageProcessor.tsProcessedMessageState 已携带 Web 的分组字段:groupedPacketsMapseenGroupKeysgroupKeysWithSectionEndexpectedBranchestoolGroupKeysdisplayGroupKeysfinalAnswerComingstopPacketSeentoolGroupspotentialDisplayGroups 等(messageProcessor.ts),并实现了 getGroupKey${turn_index}-${tab_index ?? 0}messageProcessor.ts)与幂等的 injectSectionEnd——合成 SECTION_END刻意不携带 sub_turn_index,因为 research/coding 的父级完成判定依赖这一点(messageProcessor.ts)。

section_end 合成是正确性核心,三个触发条件必须原样移植:

  1. 触发 1:真实 SECTION_END / ERROR 报文;
  2. 触发 2(turn 切换):新 turn_index 的第一个报文向所有尚未结束的 seenGroupKey 注入 section_end
  3. 触发 3(stop):第一个 stop 向所有打开中的分组注入。

已见过的 turn 内出现tab_index 不会触发触发 2。后端极少发送 section_end,步骤"完成"几乎全部由客户端合成。

四、PR 9b.2:处理器 + 节奏(pacing)hooks(Dark)

  • 目标:托管 reducer 并驱动 200ms 错峰揭示——这是两个被 lint 强制重构的位置。
  • 范围(in)usePacketProcessoruseMemo 全量重算 + 派生 toolTurnGroups / displayGroups / isComplete);usePacedTurnGroupsuseState + effect 驱动的揭示、答案门控、历史旁路;删除 prevPacedRef)。
  • 范围外:状态/派生 hooks(9b.3)、所有 UI。
  • 文件
文件 新建/修改 切片
mobile/src/hooks/timeline/usePacketProcessor.ts 新建 重算 + 分组派生 + renderComplete
mobile/src/hooks/timeline/usePacedTurnGroups.ts 新建 200ms 揭示 + 门控(重构后)
mobile/src/hooks/timeline/__tests__/* 新建 fake-timer 节奏测试(首步立即 / 200ms / stop 冲刷 / 旁路)+ 处理器派生
  • 预估规模:~590 LOC。依赖:9b.1。开关状态:N/A(dark)。
  • 合并时测试:Jest 单元 + fake timers——揭示节奏、stop 冲刷、历史旁路、答案在节奏完成前被扣留;处理器重算正确性。
  • 漂移检查点:在真机确认重算 + effect 驱动揭示与 Web 体感一致(03 §1 提到的"一帧延迟"风险);若首包渲染闪烁,需在 9b.7 接线前重新审视。

为什么必须重构:Web 的 usePacketProcessor渲染期间改写 state ref,usePacedTurnGroups 在渲染期间读取节奏 refs——两者在移动端 react-hooks/refs lint 下都非法。移动端改用 useMemo 全量重算(9a 已验证的模式,O(n)/次 flush,聊天规模下无压力)+ effect 写 useState(timer handle 只存 ref、仅 effect 中使用)。这是行为保持的重构:分组输出相同、200ms 节奏相同;渲染出的时间轴与 Web 完全一致。

五、PR 9b.3:时间轴状态 hooks(Dark)

  • 目标:纯 UI 状态派生——7 态状态机、展开、头部文本、实时计时、指标。
  • 范围(in)useTimelineUIState(7 态 + 布尔值)、useTimelineExpansion(自动折叠 + userHasToggled)、useTimelineHeader(文本映射;搜索子标签暂用通用文案)、useStreamingDuration(实时计时 + 后端冻结)、useTimelineMetricsuseTimelineStepState(memory,暂休眠)。
  • 范围外:所有 UI;搜索专属头部子标签(搜索阶段再做)。
  • 文件mobile/src/hooks/timeline/ 下的 useTimelineUIState.ts(状态机 + show/style 布尔)、useTimelineExpansion.ts(折叠 + 自动折叠)、useTimelineHeader.ts(头部文本映射)、useStreamingDuration.ts(实时流逝计时)、useTimelineMetrics.ts(步骤数 + 末步标志)、useTimelineStepState.ts(memory 提取,休眠)+ 对应 __tests__/*
  • 预估规模:~550 LOC。依赖:9b.1(类型 + 谓词)。开关状态:N/A(dark)。
  • 合并时测试:Jest 单元——每个 TimelineUIState 分支 + 派生布尔;展开的自动折叠与 userHasToggled;计时 tick/冻结。

7 态状态机(来自 Web,顺序即优先级):EMPTYDISPLAY_CONTENT_ONLYSTREAMING_PARALLEL / STREAMING_SEQUENTIALSTOPPEDCOMPLETED_EXPANDEDCOMPLETED_COLLAPSED,外加 showTintedBackgroundshowRoundedBottomshowDoneStepshowStoppedStephasDoneIndicatorshowParallelTabsshowCollapsedCompactshowCollapsedParallelisStreamingisCompletedisActivelyExecuting 等派生布尔。

六、PR 9b.4:render-prop 渲染器契约 + 最终答案迁移(Live)

  • 目标:采纳 Web 的 render-prop 契约并把最终答案路由到它——这就是"以后不重构"的那一步。
  • 范围(in)timelineContract.tsRenderType / RendererResult / MessageRenderer / FullChatState / TimelineRendererResult);findRenderer.ts(完整 13 槽优先级链——chat 已接线,reasoning 槽存在但为 null,其余标注 // PR 9x);RendererComponent.tsx(最终答案在 FULL 渲染,混合 chat+image 桩 = 9e);把 MessageTextRenderer 迁移到契约;从 registry.ts 重新导出;把 MessageRow 的答案渲染指向 RendererComponent(时间轴仍是桩)。
  • 范围外:时间轴 UI、推理渲染器。
  • 文件mobile/src/components/chat/renderers/ 下的 timelineContract.ts(契约类型)、findRenderer.ts(优先级分发)、RendererComponent.tsx(最终答案分发)、MessageTextRenderer.tsx(修改,迁移到 render-prop 契约)、registry.ts(修改,重新导出 findRenderer);mobile/src/components/chat/MessageRow.tsx(修改,答案 → RendererComponent,时间轴仍为桩);mobile/src/hooks/usePacketDisplay.ts(修改,保持暴露 processedCitedSources);__tests__/*(findRenderer 分发顺序、MessageTextRenderer render-prop、RendererComponent)。
  • 预估规模:~510 LOC。依赖:9b.1。开关状态:N/A——live 行为变更(答案路径),渲染必须完全一致。
  • 合并时测试:Jest——分发顺序(chat 优先、reasoning 最后);MessageTextRenderer 发出 children([result]);RendererComponent 渲染答案。手动:确认流式 markdown 答案 + 9a 内联引用 + Sources 栏不变。
  • 漂移检查点:本 PR 触及已上线的 PR-3/9a 代码——开工前重新确认 9a 引用链接与 CitedSources 行为,确保迁移精确保留。

render-prop 纪律(贯穿后续所有渲染器):渲染器必须调用 children(results)——绝不能直接返回自己的 View 树,否则 StepContainer 无法包裹它。RenderType 策略:最终答案路径恒为 FULL;时间轴步骤为 override ?? (isExpanded ? FULL : COMPACT),两个入口都要保留。

七、PR 9b.5:时间轴原语 + StepContainer(Dark)

  • 目标:可视步骤框架——轨道、连接线、表面、头部、折叠——加上新图标/原语。
  • 范围(in)primitives/*timelineTokens px 常量、TimelineRoot / HeaderRow / Row / IconColumn / Surface / StepContent去掉 hover);StepContainerTimelineRendererComponent(逐步展开 + renderType 派生);新图标 circle / fold / expand / check-circleowner-ASK);确认/移植三级图标 Buttonowner-ASK);StreamingMarkdown muted 变体。
  • 范围外:推理渲染器、AgentTimeline 组合、头部。
  • 文件mobile/src/components/chat/timeline/primitives/*(新建:tokens + 6 个轨道/表面/内容原语)、StepContainer.tsx(把原语组合成步骤框架)、TimelineRendererComponent.tsx(逐步展开 + renderType)、mobile/src/icons/{circle,fold,expand,check-circle}.tsx(新建,ASK owner)、StreamingMarkdown.tsx(修改,muted/compact 变体)、__tests__/*(StepContainer 折叠门控、末步连接线、错误表面;renderType 派生)。
  • 预估规模:~650 LOC。依赖:9b.4(契约)。开关状态:N/A(dark——尚未挂载)。
  • 合并时测试:RN Testing Library——StepContainer 按 renderType 渲染头/体/折叠;图标轨道几何;错误表面。
  • 漂移检查点Owner-ASK 门——UI 编码前先解决新图标移植 + 三级 Button(遵循 web-parity 原则);若 Web tokens 变化,按 03 重新核对 px 值。

几何细节(03 记录,供实现参考):轨道为 1px 线、顶部 8px 连接(首步隐藏)、20px 节点包裹、12px 图标、flex-fill(末步隐藏);StepContainer 头行高 32,含 status 文本与由 supportsCollapsible 门控的折叠控件;正文 pl-1 pb-1、右槽 34px(除非 noPaddingRight)。移动端间距为像素值(Web rem×16、Tailwind 步 ×4 烘焙为 px),所有 hover 分支删除(静止色:bg-border-01stroke-text-02bg-background-tint-00)。

八、PR 9b.6:推理渲染器(Dark)

  • 目标:唯一接线的步骤渲染器——流式推理 + 标题 + 500ms 最短思考时间 + markdown 正文。
  • 范围(in)reasoningState.ts(纯函数 extractFirstParagraph + constructCurrentReasoningState);ReasoningRenderer.tsx(render-prop;500ms 门;circle 图标;noPaddingRight不设 supportsCollapsible——owner 拍板,严格 web parity);ReasoningTextWindow.tsx(固定高度自动跟随 ScrollView);ReasoningTextSheet.tsx(全文 + 复制,经 expo-clipboard);在 findRenderer 注册 reasoning,并在它前面恢复 11 个未接线的工具谓词,使最末槽不能吞掉所有已关闭的工具组。
  • 范围外AgentTimeline 组合(9b.7);Web 的 Download 按钮(移动端无对应物)。
  • 文件mobile/src/chat/timeline/reasoningState.ts(标题提取 + delta 累积,纯)、mobile/src/components/chat/renderers/ReasoningRenderer.tsx(render-prop 渲染器 + 500ms 门)、mobile/src/components/chat/timeline/ReasoningTextWindow.tsx(maxHeight markdown 窗口)、findRenderer.ts(接线 reasoning 槽)、__tests__/*(标题提取规则、delta 累积、500ms 门 fake timers、空/前开始分支)。
  • 预估规模:~450 LOC。依赖:9b.4(契约)、9b.5(circle 图标、StreamingMarkdown muted、StepContainer 冒烟测试)。
  • 开关状态:N/A(dark——只有 9b.7 挂载时间轴后才可达)。
  • 合并时测试:Jest——reasoningState 标题 + 累积;ReasoningRenderer 500ms 门 + 分支;一个 StepContainer 包裹的冒烟渲染。
  • 漂移检查点:确认 react-native-streamdownmuted 变体的渲染可接受(颜色/边距)——接近设备问题;由 owner 的 9b.7 设备门禁覆盖。

实现要点constructCurrentReasoningState(packets) 判断 hasStart/hasEnd 并把 deltas join 成内容;extractFirstParagraph(content) 用 markdown 标题规则提取步骤标题(≤60 字符);500ms 最短思考时间用 useState(start) + effect 中的 timer ref 实现,animate 门控下限;渲染结果 { icon: SvgCircle, status: title || "Thinking", content: <ReasoningTextWindow/>, noPaddingRight: true }ReasoningTextWindow 是 192px 高(8×24)的 ScrollView,包裹 StreamingMarkdown variant="muted",流式时 onContentSizeChange → scrollToEnd 自动跟随,关闭后才允许用户滚动,内容溢出时显示 "View full text" 按钮(裁剪 View 在此不可行——见 03 §8(c) 的 Yoga AtMost 约束解释)。

九、PR 9b.7:Agent 时间轴组合(点亮 + 设备门禁)

  • 目标:组装外壳并把时间轴渲染上屏——"行走骨架"完成。
  • 范围(in):把 AgentTimeline 重写为外壳(运行状态 hooks + 头部切换 + 正文);ExpandedTimelineContent + CollapsedStreamingContent + TimelineStep;头部(StreamingHeader / CompletedHeader / StoppedHeaderParallelStreamingHeader 休眠桩);ParallelTimelineTabs 休眠(线性化)+ toolIcons;Done/Stopped 终止步骤;把 MessageRow.AssistantMessage 接成 AgentMessage 模拟体(usePacketProcessor + usePacedTurnGroups → 上方 AgentTimeline + 下方 RendererComponent 答案 + CitedSources);在 PR-3 流控制器/store 中捕获 streamingStartedAt
  • 范围外:任何工具渲染器(后续阶段);真正的并行 tab 标签页。
  • 文件
文件 新建/修改 切片
mobile/src/components/chat/AgentTimeline.tsx 修改 桩 → 完整外壳
mobile/src/components/chat/timeline/ExpandedTimelineContent.tsx 新建 TurnGroup[] → 步骤 + Done/Stopped
mobile/src/components/chat/timeline/CollapsedStreamingContent.tsx 新建 流式实时预览
mobile/src/components/chat/timeline/TimelineStep.tsx 新建 步骤合成器(children → StepContainer)
mobile/src/components/chat/timeline/headers/* 新建 Streaming/Completed/Stopped(+Parallel 休眠)
mobile/src/components/chat/timeline/ParallelTimelineTabs.tsx 新建 休眠桩(线性化)
mobile/src/components/chat/timeline/toolIcons.ts 新建 报文类型 → 图标映射
mobile/src/components/chat/MessageRow.tsx 修改 AssistantMessage → AgentMessage 模拟体
流控制器 / chatSessionStore 修改 每节点 streamingStartedAt
__tests__/* 新建 模拟推理流 → "Thinking" 步骤 → 折叠为 "Thought for Ns";点击展开;USER_CANCELLED → Stopped
  • 预估规模:~700 LOC。依赖:9b.2、9b.3、9b.5、9b.6。开关状态:N/A——时间轴正式上线
  • 合并时测试:RN Testing Library——模拟推理报文流渲染流式头部 → 步骤 → 自动折叠 → 点击展开 → Done;Stopped 路径。
  • HARD 设备门禁(owner 执行):dev 构建 + 推理模型——流式微光 + 实时计时、答案开始时自动折叠、点击展开、Done 步骤、hydrated(重开)渲染;迁移后的答案 + 9a Sources 完好;流式重渲染保持流畅(性能隔离见 04)。
  • 漂移检查点:重新确认 unify-chat-inputChatSurface 组合(PR6 重构后)是 MessageRow / AgentTimeline 的挂载点,且按会话草稿/重置规则不干扰按节点的时间轴状态。

9b.7 按建成记录(2026-08-03,"parallel tabs pulled forward"——编码前 owner 门禁)

四个 AskUserQuestion 门。 最大的一扇:owner 检查 docs/mobile-chat/ 下是否有任何地方规划了并行标签页——没有(只有下方 "After 9b" 未排期的行)——于是拍板现在就建ParallelTimelineTabsParallelStreamingHeader 以 LIVE 而非休眠状态上线,03 §8(d) 的线性化分歧废弃。这要求一个全新的移动端原语:components/ui/tabs.tsx——Opal 的 pill Tabs 的 RN 移植(Root context + List + Trigger),owner 批准用它而不是手搓一个相似物。其余门:从 Opal 1:1 移植 stop-circle(沿用 9b.5 图标先例);usePacketDisplay 折叠进 usePacketProcessor(暴露 processed,删除该 hook 及其测试),而不是每次 flush 把每个报文处理两遍;全部作为一个 PR 上线。

新增文件:components/chat/timeline/{ExpandedTimelineContent,CollapsedStreamingContent,TimelineStep,TimelineStepComposer,ParallelTimelineTabs,ShimmerText}.tsx + toolIcons.tstimeline/headers/{Streaming,Completed,Stopped,ParallelStreaming}Header.tsxtimeline/primitives/TimelineTopSpacer.tsxcomponents/ui/tabs.tsx,以及 6 个 1:1 Opal 图标移植(stop-circlebranchusercodebook-openslow-time)。修改:AgentTimeline.tsx(桩 → 完整外壳)、MessageRow.tsx(→ AgentMessage 模拟体)、hooks/timeline/usePacketProcessor.ts(+processed)、chat/interfaces.ts + chat/chatHistory.ts(+streamingStartedAt、+processingDurationSeconds)、hooks/useChatController.ts + hooks/useChatSessionController.ts(两个流开始时间戳)、lib/time.ts(+formatDurationSeconds,移植自 Opal time.ts)。约 1480 LOC——约为 500–700 区间的两倍,折叠 tabs 时 owner 已接受。

与 Web 的九项有意分歧(在 03 §8 之上的 9b.7 自身分歧)

(a) 任何地方都没有 hover——Web 的 headerIsInteractive 只切换 hover class,移动端无对应物,每个 header 自己持有 Pressable;(b) 外壳不调用 useTimelineStepState(Web 中它只被 memory tag 和那个 hover 标志消费),CompletedHeader 去掉 Web 的 memory tag/tooltip/MemoriesModal——memory 渲染器是后续阶段;(c) shimmer = 透明度脉冲ShimmerText,RN Animated,保证时间轴可 jest 渲染),而非 Web 的 background-clip:text 渐变;(d) 展开正文无入场动画(Web 的 animate-in fade-in slide-in-from-top-2)——300ms 的花哨不值得把 reanimated 放回测试路径;(e) streamingStartTime每节点 prop(而非 Web 的按会话 store 读取),恢复运行从恢复点重启计时(此客户端从未观察过该轮真正开始时间;Web 在那里根本不显示计时);(f) 移植的 Tabs 只带 pill 变体(无 contained/underline——没有消费者)并去掉滚动箭头(触摸列表靠滑动而非箭头步进);(g) TimelineStep 拆成独立模块——Web 将其私有在 ExpandedTimelineContent 内部,在移动端会形成 ExpandedTimelineContent → ParallelTimelineTabs → TimelineStep 循环;(h) getToolIcon 使用 Onyx 字形,而 Web 按自己的图标规则回退到 react-iconsFiToolcpu,对齐 chat/tools.ts 的自定义工具回退;FiListtext-lines-small)。

一并修复的问题

hydrated(历史重开)的 turn 完全没有时长,导致每次重开会话的头部都会显示 "Thought for some time"——现在把 processing_duration_seconds(后端响应里已有,models.py:232)通过 chatHistory 映射进来。

对抗性评审(4 透镜 × 3 名先反驳的怀疑者——8 项提出、6 项修复)

  • (1) HIGH——Stopped 路径在生产中不可达。 用户 stop 会在后端 stop 报文到达之前中止 reader,stopPacketSeen 永远不会翻转:该 turn 一直停留在 STREAMING ui-state(useStreamingDuration 的 250ms 间隔持续计时)直到会话重载。修复:在 runChatStreamfinally 中于中止时合成 USER_CANCELLED stop 报文buildUserCancelledStopPacket);resume 路径已自愈(它从快照重新 hydrated,后端会重放 OverallStop)。该合成函数在仓库中真实存在:streamingModels.ts——buildUserCancelledStopPacket(packets) 读取末包 placement 生成 { type: STOP, stop_reason: USER_CANCELLED }
  • (2) MEDIUM——FlashList 回收 React keysRenderStackManager.syncItem 把池化 key 交给不同的 stableId,导致滚动离屏的 turn 的组件状态(时间轴展开、活跃分支 tab、打开的 sources 表)被无关消息继承——且因 userHasToggled 锁存,该 turn 永远无法自动折叠。keyExtractor 只提供 stableId 而非 React key;修复:在 renderItem 内用 nodeId 作为 MessageRow 的 key。
  • (3) LOW ×3——chatState memo 的计数代理可能在移动端钉住过期 map(Web 靠原地修改侥幸通过)、Tabs baseline 覆盖了滑动指示器、Opal 的 pt-1 属于它的无滚动箭头分支。
  • (4) 5 个文件未做 prettier 格式化。按原样接受CitedSources 门保留 9a 的 processed.isComplete(答案收回期间 Sources 栏短暂提前出现,不值得为它改变已上线的 9a 语义)。

验证结果

tsc 干净、lint 干净(2 个既有的 _layout 警告)、prettier 干净、76 个套件 596 个 jest 用例通过(+40 个新增:强制的组合冒烟测试——推理流 → "Thinking" 步骤 → 自动折叠为 "Thought for Xs · 1 step" → 点击展开 → Done;USER_CANCELLED → Interrupted Thinking → Stopped;并行 turn → 流式头部和展开正文中各分支一个 tab,含分支切换——外加 Tabs 原语、fake timers 下 StreamingHeader 计时/冻结、getToolIconformatDurationSecondschatHistory 时长映射、streamingStartedAt 时间戳、中止时合成 stop 的回归)。

设备门禁仍欠(owner 运行):对推理模型跑 dev 构建——流式微光 + 实时计时、答案开始自动折叠、点击展开、Done 步骤、hydrated(重开)渲染显示真实 "Thought for Xs"、迁移后的答案 + 9a Sources 完好、流式重渲染保持流畅。并行标签页还需多分支运行才能看到 pill 条与滑动指示器。

十、9b 之后:后续渲染器阶段(此处不做设计)

每个后续渲染器都是零重构接缝上的独立 grill-first PR:加上该工具的 obj 接口 + 一次 findRenderer 谓词接线 + 一个返回 RendererResult 的 render-prop 渲染器(search/fetch 复用 9a 的 SourceRow)。按产品价值排序(owner 拍板):search → fetch → python/code → custom-tool → deep-research(+嵌套)→ memory,外加 parallel-tab 标签页(纯 UI,解除 ParallelTimelineTabs 休眠)以及 9c/9d/9e 各自独立。这套接缝的保证在于 9b.1 就落地了完整 PacketType 枚举(引擎/helper/集合零未来枚举改动即可编译),每个工具渲染器只加自己的 obj 接口 + 一行谓词接线——这正是"以后零重构"承诺的实现机制。

十一、贯穿全程的质量保障机制

这套 PR 路线图把质量保证内建到每一个分层里,值得作为大型跨端移植的参考范式:

  1. 逐 PR 测试矩阵:纯逻辑(分组、section_end 三触发、transformers、reasoningState、状态机)走 Jest 单元;计时与节奏走 fake timers;组件走 RN Testing Library;且所有纯逻辑必须放在 reanimated-free 模块mobile/src/chat/timeline/**),规避 "Worklets not initialized" 崩溃。
  2. 漂移检查点(drift checkpoint):每个 PR 都标出需向 Web/后端源头重新确认的假设(报文形状、packetProcessor.ts 是否变化、9a 行为、token 值),防止移植"追着移动靶"。
  3. Owner-ASK 门:新图标、新原语(三级 Button、Tabs)等一切可能偏离 Web 的决策,都明确要求先征询 owner,而不是手搓相似物。
  4. HARD 设备门禁:自动化无法覆盖的流式体验(微光、计时、自动折叠、hydrated 渲染、性能)由 owner 在真机 + 推理模型上验收。
  5. 对抗性评审:9b.7 上线前用 4 透镜 × 3 怀疑者推演,8 项问题中 6 项在合并前修复(含两个 HIGH/MEDIUM 级别的生产路径缺陷)。

对于任何"把 Web 组件体系 1:1 移植到 React Native 且不许后续重构"的工程任务,这套 7-PR 分层序列——先纯引擎、再 hooks、再契约迁移、再原语、再单渲染器、最后组合点亮——是一个经过实战验证、有明确验收口径与质量门禁的可执行蓝本。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395