feat(conversations): unify agent activity into one live rail#5141
Conversation
Mid-turn agent activity was spread across four surfaces that each had a different lifetime, and two of them outlived the answer they described. - Stop promoting between-tool narration to a persisted chat message. It is already captured as a `narration` transcript item client-side and as `TranscriptItem::Narration` server-side (kept after completion), so nothing is lost — it just stops accumulating permanently between the question and the answer that superseded it. - Render the interleaved processing transcript (narration + reasoning + tool steps + nested sub-agent runs) inline through the same `ProcessingTranscriptView` the Agent Process Source panel uses, so the rail is a windowed view of the panel rather than a second, divergent rendering. Retires the separate reasoning bubble. - Bound the rail to a fixed-height viewport during an active turn that auto-follows the newest activity, releasing on scroll-up and resuming at the bottom. - Run the payload summarizer unary instead of inheriting the parent's streaming flag. Its per-token deltas were reaching the parent event stream, so the internal "[Tool output summary — <tool>]" text was flushed as `chat_interim` and rendered to the user as part of the answer — defeating the compression step it exists to perform. - Drive the group's auto-open from the whole-turn signal rather than `isRunning`, which goes false in every gap BETWEEN tools and made the panel snap shut a beat after each result. - Show raw tool output inline for failed steps only; a successful step's output is already compressed into the final answer. Restores nested sub-agent activity on the transcript path via a `renderSubagent` prop — injected rather than imported because `SubagentActivityBlock` lives in `ToolTimelineBlock`, which imports `ProcessingTranscriptView`. This gap predated the rail change and also affected the process-source panel's transcript path. Claude-Session: https://claude.ai/code/session_01H8ohyGwZyj1No5x6zcjqhN
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughInterim narration now remains transient, while processing transcripts render interleaved activity, nested sub-agents, bounded live-following viewports, and turn-level collapse behavior. The parent summarizer executes in unary mode while sharing the parent event sink. ChangesConversation processing flow
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ChatRuntimeProvider
participant processingByThread
participant ChatThreadView
participant ToolTimelineBlock
participant ProcessingTranscriptView
ChatRuntimeProvider->>processingByThread: record interim narration
processingByThread->>ChatThreadView: provide processing transcript
ChatThreadView->>ToolTimelineBlock: pass selected thread processing
ToolTimelineBlock->>ProcessingTranscriptView: render transcript entries
ProcessingTranscriptView->>ToolTimelineBlock: render nested sub-agent activity
ToolTimelineBlock->>ToolTimelineBlock: follow live edge while turn is active
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f387dc1cb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| {transcript && transcript.length > 0 ? ( | ||
| <ProcessingTranscriptView | ||
| transcript={transcript} | ||
| entries={ordered} | ||
| renderSubagent={subagent => ( |
There was a problem hiding this comment.
Keep tool outputs reachable in transcript mode
When transcript is non-empty this branch bypasses the legacy row renderer, but ProcessingTranscriptView's tool rows only render title/detail/failure and never expose entry.result or the onViewDetails link. Because the reducer records a transcript pointer for every tool call, normal current turns with successful outputs (for example shell/test output stored on entry.result) now take this path, so the output that used to be expandable from the process trail is no longer reachable from the inline timeline or the whole-run source panel. Please carry the result/detail affordance into the transcript renderer or keep the legacy row path for entries with results.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/features/conversations/components/ProcessingTranscriptView.tsx`:
- Around line 168-174: Update the sub-agent rendering block in
ProcessingTranscriptView so renderSubagent(entry.subagent), which returns
SubagentActivityBlock, is not nested inside the inline span. Replace the wrapper
with a div or render the block as a sibling under the parent li while preserving
the existing spacing and data-testid behavior.
In `@src/openhuman/tinyagents/payload_summarizer.rs`:
- Around line 302-329: Add regression coverage for the unary invocation used by
the summarizer around invoke_with_events: create a fake model and event
recorder, invoke the child in unary mode, and assert the parent receives no
TextDelta or ThinkingDelta events while child lifecycle events are emitted. Also
verify the returned run.text() contains the expected summary output.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fa03c2f8-5aec-4b82-8e02-0c85571a7593
📒 Files selected for processing (10)
app/src/features/conversations/Conversations.tsxapp/src/features/conversations/components/AgentProcessSourcePanel.tsxapp/src/features/conversations/components/ProcessingTranscriptView.tsxapp/src/features/conversations/components/ToolTimelineBlock.tsxapp/src/features/conversations/components/__tests__/ToolTimelineBlock.test.tsxapp/src/features/conversations/utils/interimNarration.test.tsapp/src/features/conversations/utils/interimNarration.tsapp/src/providers/ChatRuntimeProvider.tsxapp/src/providers/__tests__/ChatRuntimeProvider.test.tsxsrc/openhuman/tinyagents/payload_summarizer.rs
Rehost the three Conversations.tsx edits (interim-narration filter, live-rail transcript prop, thinking-bubble removal) into ChatThreadView.tsx, which main extracted the chat rendering into. The other seven files auto-merged with the PR's changes intact; main's slice still feeds processingByThread, so the rail's data source is unchanged. Claude-Session: https://claude.ai/code/session_01H8ohyGwZyj1No5x6zcjqhN
|
Merge + rebase note: merged latest Pre-push hook bypassed with |
… span CodeRabbit: renderSubagent returns SubagentActivityBlock (a <div>); wrapping it in the label <span> produced invalid <div>-inside-<span> nesting. Move it to a <div> sibling under the <li>, indented past the icon. Claude-Session: https://claude.ai/code/session_01H8ohyGwZyj1No5x6zcjqhN
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
app/src/features/conversations/components/ChatThreadView.tsx (3)
167-182: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftComponent exceeds the file-size/SRP guideline.
This component is ~1141 lines and owns message rendering, reactions, attachment previews (image/video/file), streaming preview bubbles, the tool-timeline/processing-transcript rail, and three side panels (background processes, subagent drawer, process-source). Consider extracting cohesive pieces (e.g. a
MessageBubbleRow, anAttachmentPreviewgroup, aStreamingPreviewBlock, and the reaction-picker affordance) into their own single-responsibility modules composed here.As per coding guidelines, "Prefer files of approximately 500 lines or fewer and keep modules small, single-responsibility, and composed through clear boundaries."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/features/conversations/components/ChatThreadView.tsx` around lines 167 - 182, Refactor ChatThreadView into smaller single-responsibility modules while preserving its current behavior and public API. Extract cohesive concerns such as message rendering into MessageBubbleRow, attachment previews into an AttachmentPreview group, streaming previews into StreamingPreviewBlock, and reaction-picker UI into separate modules, then compose them from ChatThreadView; keep the existing side panels and thread-level orchestration clearly separated and reduce the component file toward the 500-line guideline.Source: Coding guidelines
1103-1125: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winUnhandled rejection on subagent cancel.
subagentApi.cancel(taskId)is awaited with notry/catch. If the RPC call throws (network error, timeout, etc.), this becomes a silent unhandled promise rejection: no diagnostic is emitted and the user gets no feedback that Cancel did nothing.🛡️ Add error handling + diagnostics
onCancel={ openSubagentEntry?.subagent && threadId ? async () => { const taskId = openSubagentEntry.subagent!.taskId; - const result = await subagentApi.cancel(taskId); - if (result.cancelled) { - dispatch(markSubagentCancelled({ threadId, taskId: result.taskId })); - } + try { + const result = await subagentApi.cancel(taskId); + if (result.cancelled) { + dispatch(markSubagentCancelled({ threadId, taskId: result.taskId })); + } + } catch (error) { + console.error('[ChatThreadView] subagent cancel failed', { taskId, error }); + } } : undefined }As per coding guidelines, "New or changed flows must include verbose, grep-friendly diagnostics covering entry/exit, branches, external calls, retries/timeouts, state transitions, and errors."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/features/conversations/components/ChatThreadView.tsx` around lines 1103 - 1125, The onCancel handler in ChatThreadView must handle failures from subagentApi.cancel instead of allowing rejected promises to escape. Wrap the cancellation call and result handling in try/catch, emit a verbose grep-friendly diagnostic containing the task identifier and error details on failure, and provide user feedback that cancellation did not complete; preserve the existing markSubagentCancelled transition only when result.cancelled is true.Source: Coding guidelines
443-480: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPass
onViewSubagentthrough thisToolTimelineBlock— otherwise subagent rows in the rail lose the drawer affordance.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/features/conversations/components/ChatThreadView.tsx` around lines 443 - 480, Pass the existing onViewSubagent callback into the ToolTimelineBlock rendered in ChatThreadView, alongside onViewDetails and onViewWholeRun, so subagent rows retain their drawer affordance.
🧹 Nitpick comments (1)
app/src/features/conversations/components/ChatThreadView.tsx (1)
256-257: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUnstable references defeat downstream
useMemos.
selectedThreadToolTimeline/selectedThreadProcessingfall back to a fresh[]literal each render (unlike the file's ownEMPTY_MESSAGES/EMPTY_TURN_TIMELINESpattern), andvisibleMessagesis never memoized before being passed as auseMemodependency intotimelineMessages— so that memo (and thepastTurnAnchorsmemo chained off it) effectively recomputes on every render, including the high-frequency re-renders that happen while a turn is actively streaming.♻️ Restore referential stability
+const EMPTY_TOOL_TIMELINE: ToolTimelineEntry[] = []; +const EMPTY_PROCESSING_TRANSCRIPT: ProcessingTranscriptItem[] = []; ... - const selectedThreadToolTimeline = threadId ? (toolTimelineByThread[threadId] ?? []) : []; - const selectedThreadProcessing = threadId ? (processingByThread[threadId] ?? []) : []; + const selectedThreadToolTimeline = threadId + ? (toolTimelineByThread[threadId] ?? EMPTY_TOOL_TIMELINE) + : EMPTY_TOOL_TIMELINE; + const selectedThreadProcessing = threadId + ? (processingByThread[threadId] ?? EMPTY_PROCESSING_TRANSCRIPT) + : EMPTY_PROCESSING_TRANSCRIPT; ... - const visibleMessages = messages.filter( - (msg, index) => !msg.extraMetadata?.hidden && !supersededInterim.has(index) - ); + const visibleMessages = useMemo( + () => + messages.filter( + (msg, index) => !msg.extraMetadata?.hidden && !supersededInterim.has(index) + ), + [messages, supersededInterim] + );Also applies to: 279-307
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/features/conversations/components/ChatThreadView.tsx` around lines 256 - 257, Stabilize the fallback references for selectedThreadToolTimeline and selectedThreadProcessing by reusing the file’s existing empty constants instead of creating new [] values on each render. Memoize visibleMessages before it is used by the timelineMessages useMemo, preserving its existing filtering/selection behavior so timelineMessages and the dependent pastTurnAnchors memo remain stable during unrelated streaming renders.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@app/src/features/conversations/components/ChatThreadView.tsx`:
- Around line 167-182: Refactor ChatThreadView into smaller
single-responsibility modules while preserving its current behavior and public
API. Extract cohesive concerns such as message rendering into MessageBubbleRow,
attachment previews into an AttachmentPreview group, streaming previews into
StreamingPreviewBlock, and reaction-picker UI into separate modules, then
compose them from ChatThreadView; keep the existing side panels and thread-level
orchestration clearly separated and reduce the component file toward the
500-line guideline.
- Around line 1103-1125: The onCancel handler in ChatThreadView must handle
failures from subagentApi.cancel instead of allowing rejected promises to
escape. Wrap the cancellation call and result handling in try/catch, emit a
verbose grep-friendly diagnostic containing the task identifier and error
details on failure, and provide user feedback that cancellation did not
complete; preserve the existing markSubagentCancelled transition only when
result.cancelled is true.
- Around line 443-480: Pass the existing onViewSubagent callback into the
ToolTimelineBlock rendered in ChatThreadView, alongside onViewDetails and
onViewWholeRun, so subagent rows retain their drawer affordance.
---
Nitpick comments:
In `@app/src/features/conversations/components/ChatThreadView.tsx`:
- Around line 256-257: Stabilize the fallback references for
selectedThreadToolTimeline and selectedThreadProcessing by reusing the file’s
existing empty constants instead of creating new [] values on each render.
Memoize visibleMessages before it is used by the timelineMessages useMemo,
preserving its existing filtering/selection behavior so timelineMessages and the
dependent pastTurnAnchors memo remain stable during unrelated streaming renders.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: de148888-976f-407e-8c3f-ce82dcb4018d
📒 Files selected for processing (2)
app/src/features/conversations/components/ChatThreadView.tsxapp/src/providers/ChatRuntimeProvider.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/providers/ChatRuntimeProvider.tsx
Summary
ProcessingTranscriptViewthe Agent Process Source panel uses. The separate reasoning bubble is retired.[Tool output summary — <tool>]text stops being rendered to the user as part of the answer.Problem
Four separate defects, all surfacing as "the agent's working notes pollute the answer":
Tool output summaries appeared in the chat.
SubagentPayloadSummarizercallsinvoke_in_parent, which inheritsparent.streaming(trueon a chat turn). The child ran the streaming loop, so its per-token deltas landed on the sharedEventSinkas parentAgentProgress::TextDelta, were buffered intopending_narration, flushed aschat_interim, and persisted as an agent message. The summarizer exists to compress a payload for the orchestrator's context; surfacing it to the user defeats the step entirely.Narration outlived its turn.
onInterimpersisted every between-tool narration ("Let me get the data for both.", "The HTML is hard to parse…") viaaddInferenceResponse({ isInterim: true }). Nothing ever removed them, so a 5-tool turn left five stale bubbles wedged between the question and the answer. Reasoning, by contrast, is wiped atchat_done— narration had a lifetime no other progress signal has.The activity panel snapped shut between tools.
autoOpenkeyed offisRunning("a tool is executing this instant"), which goes false in every gap while the agent reasons about a result before its next call. A just-delivered tool result appeared to be wiped a beat later, and a multi-tool turn flickered. fix(conversations): reset agentic task insights override on turn settle #5008 had already establishedturnActiveas the correct whole-turn signal and applied it to the override reset, but leftautoOpenbehind.Successful tool output was duplicated. Compact rows rendered a scrollable
<pre>of raw output per step, stacked above an answer that already summarized all of them.Separately,
ProcessingTranscriptViewnever readentry.subagent, so a delegated sub-agent collapsed to a single line with every child tool call hidden. This predates this PR and also affected the process-source panel's transcript path (only its legacy fallback nested them).Solution
Core (
payload_summarizer.rs) — one call swapped frominvoke_in_parenttoinvoke_with_events, which runs the child through the unary path (run_child(.., streaming = false)). Per its own contract that "leav[es] the parent's event stream unchanged" while still sharing the sink, so sub-agent lifecycle events still reach observers — only the token deltas go quiet. Mirrorsreprompt_for_required_block, already documented as deliberately silent about an internal call. Two config bits the entry point drops are both non-issues here: the childthread_id(only attributes events we no longer stream) and inheritedmax_turn_output_tokens(already enforced by theMaxTokensModelwrapper).Narration —
onInterimstops dispatchingaddInferenceResponse. Nothing is lost:streamDeltaReceivedalready coalesces everycontentdelta intoprocessingByThreadas anarrationitem, and the core persists the same thing asTranscriptItem::Narration, kept after completion so a reload replays it. The dedup guard and preview reset are retained — the latter matters more now, so the rail and the preview don't show the same text.The rail —
ToolTimelineBlockgains an optionaltranscriptprop; when non-empty it rendersProcessingTranscriptViewinside the windowed viewport, else falls back to the tool-row list for legacy snapshots that predate the transcript.Windowing — a fixed-height viewport during an active turn, following the live edge via a
ResizeObserveron the row list (catches row-arrival, auto-expansion, andtool_args_deltastreaming into an already-expanded row — the last changes no React key and would otherwise stall following mid-tool). Attached via a callback ref, notuseEffect([windowed]):windowedflips true at the start of a turn when there is nothing to show, so the component returned null, the ref was null, and the effect bailed — content arriving later never re-ran it, and no observer was ever created.Sub-agent nesting — restored via a
renderSubagentprop, injected rather than imported becauseSubagentActivityBlocklives inToolTimelineBlock, which now importsProcessingTranscriptView; importing back would be a cycle. Extracting those components into their own module is the cleaner long-term shape and is noted as follow-up.Deliberate reversals
Three existing assertions were inverted, each with an explanatory comment rather than deletion:
autoOpenonisRunningwas intentional (fix(conversations): reset agentic task insights override on turn settle #5008 documented it in a test comment). It is the direct cause of defect 3, so it is reversed; the flicker guard fix(conversations): reset agentic task insights override on turn settle #5008 actually protected — a manual toggle surviving repeatedisRunningflips within one turn — stays under test.Submission Checklist
pnpm test:coveragerun locally, exit 0 (project-wide 82.2% lines / 80.26% statements); full frontend suite green (8,692 passed / 750 files). Changed frontend lines are covered by 30+ added tests. Caveat: I did not rundiff-coverlocally, so the per-diff number is CI's to confirm. One Rust line (invoke_with_events) sits in an async fn that requires a live model, so it is exercised only end-to-end; the module's 6 existing unit tests still pass.N/A: behaviour-only change; no feature rows added, removed or renamed.## RelatedN/A: no new surface; chat activity rendering only, covered by existing smoke stepsCloses #NNN—N/A: no tracking issue; found and fixed during live staging useImpact
isInterimmessages;interimNarration.tshides them once their turn has an answer, and deliberately keeps them for a turn that died before answering, since that narration is the only record of what ran.src/openhuman/tinyagents/payload_summarizer.rs); the frontend changes are hot-reloadable.Related
turnActivesignal this extends toautoOpen), feat(conversations): per-turn process history (timeline refactor Phases 1–2, 4–5) #4612 (introduced compact-row output), fix(flows): persist agentic task insights expand state across copilot turns #4942 (the sticky disclosure override)SubagentActivityBlock(+ThoughtBlock,ToolCallRow) into their own module and import it directly, replacing therenderSubagentinjection.failure(why/next) is stored on the entry but never rendered inline —ToolFailureLinesfires only for sub-agent children, the processing transcript, and the sub-agent drawer.MIN_INTERIM_NARRATION_CHARSgating and thechat_interimevent itself are now only used for the dedup key and preview reset; the event could be retired core-side.AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
feat/agent-activity-rail1f387dc1cValidation Run
pnpm --filter openhuman-app format:check— "All matched files use Prettier code style!"pnpm typecheck— cleanToolTimelineBlock73 passed ·ChatRuntimeProvider70 passed ·interimNarration6 passed · full suite 8,692 passed / 750 filescargo fmt --checkclean ·GGML_NATIVE=OFF cargo checkclean ·cargo test --lib payload_summarizer6 passed ·progress_bridge12 passedN/A: app/src-tauri untouchedValidation Blocked
command:end-to-end verification of the summarizer silenceerror:n/a — not a failure; the changed Rust line runs only with a live model on a large tool payloadimpact:behaviour confirmed by readingrun_child's streaming contract and theTranscriptItempersistence path; not reproduced in an automated testBehavior Changes
Parity Contract
turnActiveomitted → falls back toisRunning, so settled/past-turn renders are unaffected;expandAllRows(process panel) never windows;renderSubagentomitted → rows degrade to one line rather than breaking;ResizeObserverabsent → auto-follow is skipped, not fatal.Duplicate / Superseded PR Handling
Summary by CodeRabbit
New Features
Improvements