背景
用户进入直接 Subagent takeover 或 Workflow child transcript 时,希望像阅读主 Pi 会话一样阅读一段连续对话:用户输入、Assistant Markdown、thinking、工具活动与结果使用一致的视觉语言;新内容到达时默认自动跟随底部,用户主动上滚后则保留阅读位置。
当前两条 child UI 已经具备数据和实时刷新能力,但使用了两套 renderer 与两套滚动状态,因此行为割裂。
当前事实
基于 main@dbaacf4 与当前依赖 Pi 0.84.1。
1. Pi 主会话的参考语义
Pi 主会话分别用 AssistantMessageComponent 和 ToolExecutionComponent 处理 Assistant Markdown/thinking 与工具生命周期:
fullscreen 主 transcript 使用 ScrollView({ follow: "end" }):
这应当是 child viewport 的交互参考。
2. Direct Subagent:格式较接近,但滚动状态不等价
Direct takeover 已经有较完整的 transcript renderer:Assistant 与 thinking 使用 Markdown,live assistant/tool 单独投影,finalized item 有缓存:
但 takeover 用“距底部行数”保存滚动位置:
因此它能默认追尾,但用户上滚后若新行继续追加,相同的 bottom-relative offset 会让可见内容向后漂移,不能像 Pi ScrollView 一样稳定保持原来的阅读锚点。
3. Workflow child:独立日志样式,且默认从顶部打开
Workflow 将 Pi messages 归一化为持久化 TranscriptEntry[],保留 role、text、toolCallId、timing 等有限事实;它不是完整 Pi Tool Component:
Dashboard 又单独实现了一套角色标签/箭头/文本换行 renderer:
滚动位置使用 top-relative transcriptScroll,进入详情时重置为 0:
所以运行中的 Workflow child 打开后不会默认看到最新输出,也没有 follow/pause/resume 状态。
结论
可以做到与主会话一致的视觉语义和滚动语义 ,但不应直接实例化 Pi 的内部 AssistantMessageComponent / ToolExecutionComponent:
这些类不是稳定的扩展 seam;
ToolExecutionComponent 依赖原始 ToolDefinition、partial/expanded 状态和 live renderer state;
child transcript 已经过归一化和大小裁剪,历史记录不具备重建原生组件所需的全部事实;
强行重建会把 operator UI 与 Pi 内部实现、持久化版本和工具注册表耦合。
正确边界是:保留 Direct 与 Workflow 各自的 canonical/read-model 数据,用两个薄 adapter 投影到一个共享的、纯 operator-facing transcript document;共享 renderer 与 viewport state 只消费该 document。
建议方案
A. 一个共享 transcript renderer
下沉 Direct 当前较成熟的 Markdown/tool rendering 能力到 extensions/shared/,暴露一个小接口,例如:
renderAgentTranscript ( document , width , theme , { now } ) : string[ ]
两条 adapter:
SubagentSnapshot -> AgentTranscriptDocument
Workflow AgentRecord/TranscriptEntry[] -> AgentTranscriptDocument
共享 document 只描述 UI 所需的确定事实:user、assistant text、thinking/redacted、tool call/result、live phase、queued input、cwd。不要让 renderer 读取 Session、activeRuns 或 artifact 文件。
第一版不迁移 Direct snapshot 或 Workflow transcripts.json 的持久化 schema;旧 Workflow artifact 继续由 adapter 读取。
B. 一个共享 follow-to-end viewport state
共享一个无 TUI 副作用的 viewport 状态模块,语义对齐 Pi ScrollView:
初始 / followingEnd=true
新行到达 -> scrollTop 移到新的 maxScrollTop
用户向上滚动
followingEnd=false
新行到达 -> 保持绝对 scrollTop,阅读锚点不动
用户滚到底部、按 G/End 或显式 scrollToEnd
followingEnd=true
Direct takeover 与 Workflow dashboard 可以保留各自 header、输入框和导航层级,只共用 body renderer 与 viewport state。
C. 与 #93 的关系
#93 是本 Issue 中“工具活动行”的子集。本 Issue 不应再造第三套工具 formatter:共享 transcript renderer 应消费 #93 的紧凑 activity projection,或者在同一 PR 中完成两者。
#93 提到的“Pi 原生展开”只有在原始证据仍可达且数据足够时才能提供;本 Issue 不得根据被裁剪的 preview 伪造完整原生 Tool Component。完整证据仍由 child session 文件、Workflow artifact 和已有结果恢复路径负责。
目标体验
user:与主会话相同的用户消息层级;
assistant:Markdown,而不是 Assistant: 日志标签;
thinking:muted/italic,redacted 仍明确显示;
tool:使用 统一 Subagent 与 Workflow child 的图标化单行工具活动视图 #93 的紧凑 pending/success/error 活动行;
tool result:必要的 bounded preview,错误明确但不淹没最终回答;
Direct 与 Workflow 可保留不同 header/badge,conversation body 必须共用视觉规则;
默认显示最新输出并自动跟随;手动上滚后不抢回底部;恢复 follow 有明确按键和状态提示。
验收标准
共享渲染
滚动
边界保护
非目标
实施顺序建议
先抽出纯 viewport state,并用追加/缩短/resize 的状态测试锁定 follow 语义;
把 Direct renderer 下沉为共享 renderer,确保 Direct 行为不退化;
增加 Workflow adapter,替换 transcriptRows();
接入 统一 Subagent 与 Workflow child 的图标化单行工具活动视图 #93 的共享 tool activity projection;
做 Direct + Workflow 真实 TUI smoke。
背景
用户进入直接 Subagent takeover 或 Workflow child transcript 时,希望像阅读主 Pi 会话一样阅读一段连续对话:用户输入、Assistant Markdown、thinking、工具活动与结果使用一致的视觉语言;新内容到达时默认自动跟随底部,用户主动上滚后则保留阅读位置。
当前两条 child UI 已经具备数据和实时刷新能力,但使用了两套 renderer 与两套滚动状态,因此行为割裂。
当前事实
基于
main@dbaacf4与当前依赖 Pi0.84.1。1. Pi 主会话的参考语义
Pi 主会话分别用
AssistantMessageComponent和ToolExecutionComponent处理 Assistant Markdown/thinking 与工具生命周期:fullscreen 主 transcript 使用
ScrollView({ follow: "end" }):这应当是 child viewport 的交互参考。
2. Direct Subagent:格式较接近,但滚动状态不等价
Direct takeover 已经有较完整的 transcript renderer:Assistant 与 thinking 使用 Markdown,live assistant/tool 单独投影,finalized item 有缓存:
但 takeover 用“距底部行数”保存滚动位置:
scrollOffset = 0表示贴底因此它能默认追尾,但用户上滚后若新行继续追加,相同的 bottom-relative offset 会让可见内容向后漂移,不能像 Pi
ScrollView一样稳定保持原来的阅读锚点。3. Workflow child:独立日志样式,且默认从顶部打开
Workflow 将 Pi messages 归一化为持久化
TranscriptEntry[],保留 role、text、toolCallId、timing 等有限事实;它不是完整 Pi Tool Component:Dashboard 又单独实现了一套角色标签/箭头/文本换行 renderer:
滚动位置使用 top-relative
transcriptScroll,进入详情时重置为0:rows[transcriptScroll:...]所以运行中的 Workflow child 打开后不会默认看到最新输出,也没有 follow/pause/resume 状态。
结论
可以做到与主会话一致的视觉语义和滚动语义,但不应直接实例化 Pi 的内部
AssistantMessageComponent/ToolExecutionComponent:ToolExecutionComponent依赖原始 ToolDefinition、partial/expanded 状态和 live renderer state;正确边界是:保留 Direct 与 Workflow 各自的 canonical/read-model 数据,用两个薄 adapter 投影到一个共享的、纯 operator-facing transcript document;共享 renderer 与 viewport state 只消费该 document。
建议方案
A. 一个共享 transcript renderer
下沉 Direct 当前较成熟的 Markdown/tool rendering 能力到
extensions/shared/,暴露一个小接口,例如:两条 adapter:
SubagentSnapshot -> AgentTranscriptDocumentWorkflow AgentRecord/TranscriptEntry[] -> AgentTranscriptDocument共享 document 只描述 UI 所需的确定事实:user、assistant text、thinking/redacted、tool call/result、live phase、queued input、cwd。不要让 renderer 读取 Session、activeRuns 或 artifact 文件。
第一版不迁移 Direct snapshot 或 Workflow
transcripts.json的持久化 schema;旧 Workflow artifact 继续由 adapter 读取。B. 一个共享 follow-to-end viewport state
共享一个无 TUI 副作用的 viewport 状态模块,语义对齐 Pi
ScrollView:Direct takeover 与 Workflow dashboard 可以保留各自 header、输入框和导航层级,只共用 body renderer 与 viewport state。
C. 与 #93 的关系
#93 是本 Issue 中“工具活动行”的子集。本 Issue 不应再造第三套工具 formatter:共享 transcript renderer 应消费 #93 的紧凑 activity projection,或者在同一 PR 中完成两者。
#93 提到的“Pi 原生展开”只有在原始证据仍可达且数据足够时才能提供;本 Issue 不得根据被裁剪的 preview 伪造完整原生 Tool Component。完整证据仍由 child session 文件、Workflow artifact 和已有结果恢复路径负责。
目标体验
Assistant:日志标签;验收标准
共享渲染
transcripts.json无需迁移即可渲染;滚动
followingEnd=true时追加行会继续显示最新尾部;followingEnd=false,追加多行仍保持同一个 top row/阅读锚点;G或End恢复 follow;g/ Home 到顶部但不自动恢复;边界保护
bun run check与bun run test通过;非目标
实施顺序建议
transcriptRows();