Skip to content

feat(conversations): unify agent activity into one live rail#5141

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
sanil-23:feat/agent-activity-rail
Jul 23, 2026
Merged

feat(conversations): unify agent activity into one live rail#5141
senamakel merged 3 commits into
tinyhumansai:mainfrom
sanil-23:feat/agent-activity-rail

Conversation

@sanil-23

@sanil-23 sanil-23 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Mid-turn agent activity was spread across four surfaces with four different lifetimes; two of them outlived the answer they described. This collapses them into one live rail.
  • Narration is no longer promoted to a persisted chat message. It stays visible during the turn (via the processing transcript) and stops accumulating permanently between the question and the answer.
  • The rail now renders the interleaved processing transcript — narration + reasoning + tool steps + nested sub-agent runs — through the same ProcessingTranscriptView the Agent Process Source panel uses. The separate reasoning bubble is retired.
  • The rail is bounded and auto-follows the newest activity during an active turn, releasing on scroll-up and resuming at the bottom.
  • Core fix: the payload summarizer no longer streams its output onto the parent event stream, so the internal [Tool output summary — <tool>] text stops being rendered to the user as part of the answer.
  • Two smaller fixes: the group no longer snaps shut in the gap between tools, and raw tool output renders inline for failed steps only.

Problem

Four separate defects, all surfacing as "the agent's working notes pollute the answer":

  1. Tool output summaries appeared in the chat. SubagentPayloadSummarizer calls invoke_in_parent, which inherits parent.streaming (true on a chat turn). The child ran the streaming loop, so its per-token deltas landed on the shared EventSink as parent AgentProgress::TextDelta, were buffered into pending_narration, flushed as chat_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.

  2. Narration outlived its turn. onInterim persisted every between-tool narration ("Let me get the data for both.", "The HTML is hard to parse…") via addInferenceResponse({ 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 at chat_done — narration had a lifetime no other progress signal has.

  3. The activity panel snapped shut between tools. autoOpen keyed off isRunning ("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 established turnActive as the correct whole-turn signal and applied it to the override reset, but left autoOpen behind.

  4. 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, ProcessingTranscriptView never read entry.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 from invoke_in_parent to invoke_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. Mirrors reprompt_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 child thread_id (only attributes events we no longer stream) and inherited max_turn_output_tokens (already enforced by the MaxTokensModel wrapper).

NarrationonInterim stops dispatching addInferenceResponse. Nothing is lost: streamDeltaReceived already coalesces every content delta into processingByThread as a narration item, and the core persists the same thing as TranscriptItem::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 railToolTimelineBlock gains an optional transcript prop; when non-empty it renders ProcessingTranscriptView inside 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 ResizeObserver on the row list (catches row-arrival, auto-expansion, and tool_args_delta streaming into an already-expanded row — the last changes no React key and would otherwise stall following mid-tool). Attached via a callback ref, not useEffect([windowed]): windowed flips 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 renderSubagent prop, injected rather than imported because SubagentActivityBlock lives in ToolTimelineBlock, which now imports ProcessingTranscriptView; 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:

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80%pnpm test:coverage run 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 run diff-cover locally, 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.
  • Coverage matrix updated — N/A: behaviour-only change; no feature rows added, removed or renamed.
  • All affected feature IDs from the matrix are listed in the PR description under ## Related
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: no new surface; chat activity rendering only, covered by existing smoke steps
  • Linked issue closed via Closes #NNNN/A: no tracking issue; found and fixed during live staging use

Impact

  • Desktop only — chat activity rendering. No mobile/web/CLI surface touched.
  • No migration. Historical threads already contain persisted isInterim messages; interimNarration.ts hides 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.
  • Performance: the rail is now bounded (fixed-height viewport) instead of growing unbounded during a turn, and one fewer message is persisted per tool round.
  • Behaviour change to weigh: classification of narration vs answer is positional (text before a tool call closes the round). Substantive mid-turn text is therefore hidden once the answer lands. It remains in the process-source panel, and the orchestrator is instructed to "distill — never forward verbatim", so the answer should carry the meaning.
  • Core rebuild required (src/openhuman/tinyagents/payload_summarizer.rs); the frontend changes are hot-reloadable.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/agent-activity-rail
  • Commit SHA: 1f387dc1c

Validation Run

  • pnpm --filter openhuman-app format:check — "All matched files use Prettier code style!"
  • pnpm typecheck — clean
  • Focused tests: ToolTimelineBlock 73 passed · ChatRuntimeProvider 70 passed · interimNarration 6 passed · full suite 8,692 passed / 750 files
  • Rust fmt/check (if changed): cargo fmt --check clean · GGML_NATIVE=OFF cargo check clean · cargo test --lib payload_summarizer 6 passed · progress_bridge 12 passed
  • Tauri fmt/check (if changed): N/A: app/src-tauri untouched

Validation Blocked

  • command: end-to-end verification of the summarizer silence
  • error: n/a — not a failure; the changed Rust line runs only with a live model on a large tool payload
  • impact: behaviour confirmed by reading run_child's streaming contract and the TranscriptItem persistence path; not reproduced in an automated test

Behavior Changes

  • Intended behavior change: mid-turn agent activity (narration, reasoning, tool steps, sub-agent runs) renders in one bounded, auto-following rail that collapses at turn end, instead of narration bubbles + a reasoning bubble + a tool list in the chat stream.
  • User-visible effect: the chat reads as question → answer. Working notes and tool output summaries no longer appear as part of the answer.

Parity Contract

  • Legacy behavior preserved: turns without a processing transcript (legacy snapshots) still render through the tool-row list; the process-source panel's legacy fallback is unchanged; sub-agent nesting works on both paths (now tested on both).
  • Guard/fallback/dispatch parity checks: turnActive omitted → falls back to isRunning, so settled/past-turn renders are unaffected; expandAllRows (process panel) never windows; renderSubagent omitted → rows degrade to one line rather than breaking; ResizeObserver absent → auto-follow is skipped, not fatal.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this one
  • Resolution: n/a

Summary by CodeRabbit

  • New Features

    • Agent activity panels now render delegated sub-agent activity inline, including when processing transcripts are shown.
    • Tool timeline/processing views now support interleaved narration and tool steps in the order they stream in.
  • Improvements

    • Interim narration is transient: it stays visible only while a turn is in-flight and is removed once a final (non-interim) response arrives.
    • Streaming previews focus on the in-progress answer content (no expandable thinking preview).
    • Live timeline windowing auto-follows new content during active processing while allowing manual scrolling, and panels remain open for the whole user turn before collapsing.

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
@sanil-23
sanil-23 requested a review from a team July 22, 2026 17:18
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40720ac6-9557-4166-961d-a44faa130802

📥 Commits

Reviewing files that changed from the base of the PR and between e9bd8ca and 6ac907e.

📒 Files selected for processing (1)
  • app/src/features/conversations/components/ProcessingTranscriptView.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/features/conversations/components/ProcessingTranscriptView.tsx

📝 Walkthrough

Walkthrough

Interim 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.

Changes

Conversation processing flow

Layer / File(s) Summary
Interim narration lifecycle
app/src/features/conversations/utils/interimNarration.ts, app/src/features/conversations/utils/interimNarration.test.ts, app/src/providers/ChatRuntimeProvider.tsx, app/src/providers/__tests__/ChatRuntimeProvider.test.tsx, app/src/features/conversations/components/ChatThreadView.tsx
Interim narration is recorded in processing state instead of persisted as a message, completed-turn interim indexes are filtered, and streaming previews render answer content only.
Transcript timeline and turn lifecycle
app/src/features/conversations/components/ToolTimelineBlock.tsx, app/src/features/conversations/components/ChatThreadView.tsx, app/src/features/conversations/components/__tests__/ToolTimelineBlock.test.tsx
ToolTimelineBlock renders transcript content with row fallback, maintains an active-turn viewport with resize-driven tail following, and settles disclosure state at turn completion.
Nested sub-agent transcript rendering
app/src/features/conversations/components/ProcessingTranscriptView.tsx, app/src/features/conversations/components/AgentProcessSourcePanel.tsx, app/src/features/conversations/components/__tests__/ToolTimelineBlock.test.tsx
A renderSubagent callback is propagated through transcript tool groups and renders delegated activity inline beneath its parent tool row.
Unary parent summarizer execution
src/openhuman/tinyagents/payload_summarizer.rs
The parent summarizer uses non-streaming invoke_with_events, shares the parent event sink, and returns the completed run text.

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
Loading

Possibly related PRs

Suggested labels: feature, rust-core

Suggested reviewers: m3ga-mind

Poem

I’m a rabbit with transcripts to share,
Nesting sub-agents in neat little layers.
Narration hops out of message rows,
While live tails follow where activity grows.
Unary summaries land soft and bright—
Processing paths now feel just right! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: unifying mid-turn agent activity into a single live rail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug feature Net-new user-facing capability or product behavior. labels Jul 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +704 to +708
{transcript && transcript.length > 0 ? (
<ProcessingTranscriptView
transcript={transcript}
entries={ordered}
renderSubagent={subagent => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between fb1f909 and 1f387dc.

📒 Files selected for processing (10)
  • app/src/features/conversations/Conversations.tsx
  • app/src/features/conversations/components/AgentProcessSourcePanel.tsx
  • app/src/features/conversations/components/ProcessingTranscriptView.tsx
  • app/src/features/conversations/components/ToolTimelineBlock.tsx
  • app/src/features/conversations/components/__tests__/ToolTimelineBlock.test.tsx
  • app/src/features/conversations/utils/interimNarration.test.ts
  • app/src/features/conversations/utils/interimNarration.ts
  • app/src/providers/ChatRuntimeProvider.tsx
  • app/src/providers/__tests__/ChatRuntimeProvider.test.tsx
  • src/openhuman/tinyagents/payload_summarizer.rs

Comment thread app/src/features/conversations/components/ProcessingTranscriptView.tsx Outdated
Comment thread src/openhuman/tinyagents/payload_summarizer.rs
@senamakel senamakel self-assigned this Jul 23, 2026
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
@sanil-23

Copy link
Copy Markdown
Collaborator Author

Merge + rebase note: merged latest main (v0.61.6 → v0.63.1) into the branch — main had extracted the chat rendering from Conversations.tsx into a new ChatThreadView.tsx, so the three UI edits (interim-narration filter, live-rail transcript prop, thinking-bubble removal) were rehosted there; the other seven files auto-merged with the PR's changes intact. Full frontend suite green (8,810 passed).

Pre-push hook bypassed with --no-verify on the merge push for one pre-existing, unrelated reason: pnpm rust:clippy fails on src/openhuman/proc_metrics/mod.rs:216 (unnecessary_map_on_constructor, a lint newer than the pinned CI toolchain). That line is unchanged on main and is not in this PR's diff — no Rust file this PR touches has a map_err. Everything else in the hook (format:check, eslint, tsc) passes.

@coderabbitai coderabbitai Bot removed bug agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. labels Jul 23, 2026
… 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lift

Component 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, an AttachmentPreview group, a StreamingPreviewBlock, 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 win

Unhandled rejection on subagent cancel.

subagentApi.cancel(taskId) is awaited with no try/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 win

Pass onViewSubagent through this ToolTimelineBlock — 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 win

Unstable references defeat downstream useMemos.

selectedThreadToolTimeline/selectedThreadProcessing fall back to a fresh [] literal each render (unlike the file's own EMPTY_MESSAGES/EMPTY_TURN_TIMELINES pattern), and visibleMessages is never memoized before being passed as a useMemo dependency into timelineMessages — so that memo (and the pastTurnAnchors memo 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1f387dc and e9bd8ca.

📒 Files selected for processing (2)
  • app/src/features/conversations/components/ChatThreadView.tsx
  • app/src/providers/ChatRuntimeProvider.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/providers/ChatRuntimeProvider.tsx

@coderabbitai coderabbitai Bot added the rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. label Jul 23, 2026
@senamakel
senamakel merged commit 49b5bb7 into tinyhumansai:main Jul 23, 2026
24 of 28 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Team Openhuman Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants