Skip to content

[chore] Split AgentConversation into hooks and views - #5569

Merged
mmabrouk merged 15 commits into
release/v0.106.2from
fe-chore/agent-conversation-split
Jul 31, 2026
Merged

[chore] Split AgentConversation into hooks and views#5569
mmabrouk merged 15 commits into
release/v0.106.2from
fe-chore/agent-conversation-split

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

Context

AgentConversation.tsx had grown to 2692 lines. One component body held the chat stream, two scroll engines, history hydration, onboarding, the composer, attachments, voice input, and the whole transcript render. Touching any one of those meant reading all of them, and the file was still growing (two feature branches landed in it this cycle).

Changes

The blocker was never the length. It was four mutable scroll refs (stick, armBottom, animateBottom, programmaticScroll) written from five call sites and read by two separate sets of effects. Nothing could be lifted out while those refs were shared ambient state.

useScrollIntent now owns them behind three verbs, one per call-site shape:

armGlide()   submit: park follow, arm ONE animated pin to the bottom
armJump()    restore/adopt: arm ONE instant jump, follow untouched
follow()     a send that leaves now (queue release, channelled run)

Producers state intent. They never touch a ref. With that seam the rest moves out whole:

assets/      conversationLayout, runError, messageParts, scrollGeometry
hooks/       useScrollIntent, useTranscriptScroll, useVirtuosoTranscript,
             useAgentChatSession, useSessionHydration, useComposerDraft,
             useComposerAttachments, useVoiceComposer, useTurnInspector,
             useOnboardingChat, useFirstRunSeed
components/  MessageRow, TurnActivity, AgentTurn, AgentTranscript,
             TranscriptPlaceholder, AgentComposerDock

AgentConversation.tsx is 679 lines and reads as a composition root.

Two changes are not pure moves, both deliberate:

AgentTurn is memoized. Previously renderMessage built every row inline, so each streamed token re-rendered every settled turn above the active one. Turn numbers are also precomputed once per message set. The old turnOfAssistant rescanned the message list per rendered row, which made the transcript O(n^2) on every commit.

useComposerAttachments returns bindDropTarget(isBlocked) rather than a static handler object. The blocked-drop guard depends on the recorder, which depends on addFiles, which would be circular. The predicate is read at event time, so no ref crosses the boundary.

Tests

  • pnpm --filter @agenta/oss exec tsc --noEmit passes with zero errors, as do @agenta/entity-ui and @agenta/ee.
  • pnpm lint-fix across the repo makes no changes. Prettier clean.
  • Effect bodies, dependency arrays and useState lazy initializers moved verbatim, comments included. Several of those comments document real races and StrictMode hazards, so they were not rewritten.
  • This branch was rebuilt against current release/v0.106.1 rather than rebased, because the voice input and file upload features landed inside the regions this refactor deletes. Verified line by line: of 357 lines those features added, every behavioural one is present. NEXT_PUBLIC_AGENT_VOICE_INPUT and NEXT_PUBLIC_AGENT_FILE_UPLOADS both still reach live code.

Not runtime verified. The gates above are static.

What to QA

  • Send a message while scrolled up. The jump pill appears, and clicking it glides to the bottom.
  • Ask something with a long answer. The question pins to the top and holds there while the answer streams in.
  • Expand a tool card while reading scrolled-up history. Your place holds, the view does not jump.
  • Reload mid approval. The queue holds new sends, and answering the gate resumes the run.
  • Record a voice message with NEXT_PUBLIC_AGENT_VOICE_INPUT on. Empty composer sends it. Composer with text attaches it instead.
  • Drag a blocked file type onto the panel. The tab does not navigate away and the cursor shows "no drop".
  • Regression: switch sessions and switch revisions. The transcript and composer draft survive both.

@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jul 30, 2026
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Jul 31, 2026 7:20pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e7929ef7-55a2-42d7-870a-b09ded3ae8b6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added smoother chat transcripts with virtualization, scroll anchoring, and a "Jump to latest" control.
    • Improved composer support for attachments, drag-and-drop uploads, voice dictation, queued messages, and approval prompts.
    • Enhanced onboarding with templates, optimistic first messages, "Continue in IDE," and start-over actions.
    • Added clearer activity indicators, turn inspection, resend controls, and in-chat run error messages.
  • Bug Fixes

    • Improved session restoration, message persistence, and handling of interrupted or resumed runs.
    • Added clearer handling for unreadable attachments.

Walkthrough

AgentConversation is decomposed into session, onboarding, composer, transcript, scrolling, attachment, voice, inspection, and turn-rendering hooks and components. New helpers handle layout, message visibility, errors, file-read failures, hydration, virtualization, and scroll intent.

Changes

Agent chat refactor

Layer / File(s) Summary
Session lifecycle and hydration
web/oss/src/components/AgentChatSlice/AgentConversation.tsx, hooks/useAgentChatSession.ts, hooks/useSessionHydration.ts, assets/runError.ts
Session streaming, hydration, persistence, error stamping, cancellation, committed-revision handling, and settle-time activity updates are delegated to session hooks.
Onboarding and submission orchestration
web/oss/src/components/AgentChatSlice/AgentConversation.tsx, hooks/useOnboardingChat.ts, hooks/useFirstRunSeed.ts
Template provenance, first-run prompts, optimistic onboarding, IDE handoff, approval gating, queue submission, and cleanup are centralized.
Composer state and dock
components/AgentComposerDock.tsx, hooks/useComposerDraft.ts, hooks/useComposerAttachments.ts, hooks/useVoiceComposer.ts, assets/files.ts
The composer handles drafts, queues, approvals, attachments, unreadable-file reporting, voice recording, onboarding actions, and rich input rendering.
Transcript layout and scrolling
components/AgentTranscript.tsx, components/TranscriptPlaceholder.tsx, hooks/useTranscriptScroll.ts, hooks/useVirtuosoTranscript.tsx, hooks/useScrollIntent.ts, assets/conversationLayout.ts, assets/scrollGeometry.ts
Transcript rendering supports virtualized and native scrolling with restoration, live-edge following, anchor preservation, placeholders, edge fades, and jump-to-latest controls.
Turn rendering and inspection
components/AgentTurn.tsx, components/MessageRow.tsx, components/TurnActivity.tsx, hooks/useTurnInspector.ts, assets/messageParts.ts
Message rows, activity indicators, inspection controls, resend actions, visibility checks, turn numbering, and offscreen rendering are extracted into reusable components and helpers.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main refactor of AgentConversation into hooks and view components.
Description check ✅ Passed The description directly explains the refactor, its design changes, validation results, and required QA scenarios.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fe-chore/agent-conversation-split

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@dosubot dosubot Bot added Frontend refactoring A code change that neither fixes a bug nor adds a feature labels Jul 30, 2026
AgentConversation.tsx: 2692 -> 679 lines, across 22 modules.

The seam that made this possible: four mutable scroll refs (stick,
armBottom, animateBottom, programmaticScroll) were written from five call
sites and read by two sets of effects. useScrollIntent owns them behind
three verbs — armGlide() on submit, armJump() on restore/adopt, follow()
for a send that leaves now — so producers never touch raw refs and the two
scroll engines can be lifted out whole.

  assets/      conversationLayout, runError, messageParts, scrollGeometry
  hooks/       useScrollIntent, useTranscriptScroll, useVirtuosoTranscript,
               useAgentChatSession, useSessionHydration, useComposerDraft,
               useComposerAttachments, useVoiceComposer, useTurnInspector,
               useOnboardingChat, useFirstRunSeed
  components/  MessageRow, TurnActivity, AgentTurn, AgentTranscript,
               TranscriptPlaceholder, AgentComposerDock

AgentTurn is memoized and turn numbers are precomputed once per message
set — previously every settled turn re-rendered on each streamed token and
the turn lookup was an O(n^2) scan.

Split against current content so the voice-input and file-upload features
survive: useVoiceComposer is a new module for the recorder state (it has no
home in the pre-feature structure), and useComposerAttachments owns
uploadsEnabled plus bindDropTarget(isBlocked) — a predicate read at event
time, since the blocked-drop guard depends on the recorder which depends on
addFiles. Verified line-by-line against the upstream diff.
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-07-31T19:19:11.202Z

The agent chat's session history, open-tab list, active tab and cached
transcripts are all `atomWithStorage` atoms. jotai's default storage
subscribes to the window `storage` event, so every write in one browser tab
replaced those records live in every other tab of the same origin.

The open-tab list drives the antd `Tabs` items, so an incoming replacement
could remove a tab and unmount its `AgentConversation`. That orphans the
`useChat` stream mid-turn, and since a transcript is only persisted once its
stream settles, the in-flight turn was lost. Switching browser tabs made it
worse: both session queries revalidate on window focus, and the reconcile
that follows writes to all three keys.

These four stores now use localStorage with `subscribe` removed, so a tab
owns its own view. Cross-tab awareness already has a correct channel: the
server reconcile on focus, plus renames pushed to the durable stream header.
The localStorage push was a redundant second channel that replaced whole
records instead of merging them.

Storage stays shared, so a reload still picks up the last write.
…004]

A browser that opened a session while another browser was mid-turn cached a
partial transcript, and no amount of refreshing ever fixed it. The durable
record log had the complete turn the whole time.

The adoption guard asked "does the server have more MESSAGES than I do?".
`transcriptToMessages` only closes a message on a `done` record and folds a
paused turn into its resume, so a turn that grows in place (tool results
landing, an approval round-trip completing) keeps the same message count from
start to finish. The guard concluded the server was not ahead and kept the
partial copy, on every subsequent reload.

The record log is append-only and ordered, so record count is an exact test.
`loadSessionMessages` now returns the count it built the transcript from, and
that watermark is persisted next to the cached messages. The guard adopts when
the log grew past the watermark, with a message-count floor so ingest lag can
never trade a longer local transcript for a shorter server one.

Both guards now share one rule (`shouldAdoptServerTranscript`): the cache-miss
hydration path carried the same blind spot and would have stayed broken if only
the revalidate path were fixed. The old "local tail paused, server tail
complete" special case is deleted rather than extended, since a watermark sees
that growth directly.

The watermark is cleared when a turn goes live locally (we cannot know what the
runner logged for it), so the next open re-syncs from the log. That clearing
effect must stay declared above the persist effect; effects fire in declaration
order, and that is what stops a locally-extended transcript being filed under a
server watermark.

Messages and watermarks can only move together: one writer sets both, and a
single `dropSessionMessages` helper is now the only deletion path, covering the
quota-eviction loop that previously shed transcripts with no way to shed their
watermarks.

Caches written before this change have no watermark, which reads as 0, so they
adopt the server copy once and are correct from then on.
A session being driven from another browser or tab gave no sign anything was
happening. It looked identical to an idle one, so the only feedback was the
transcript silently not changing.

There is no push channel to browsers today: the runner publishes every event to
Redis, but the only consumer is the ingest worker that writes them to the DB.
True live following would need an SSE endpoint with per-session auth, which is
out of scope here.

Instead the conversation now polls the durable record log while the backend
reports a live run that this browser is not driving, and the adoption guard
decides whether anything changed. A strip above the composer says so, reusing
the session bar's `running` dot so the two read as one signal.

The poll chains its timeouts rather than using setInterval, since the log is
large and backend-slow and a slow fetch must not stack requests on itself. It
backs off from 15s to 60s while the log is quiet and resets on real growth:
`is_running` is Redis-TTL'd at an hour and the runner only clears it at turn
end, so a runner that dies mid-turn would otherwise leave a browser refetching
the full log every 15s for that hour. Backing off rather than stopping keeps a
genuinely long turn followed, since a slow tool call emits no records until it
returns.

Background adoptions do not jump to the live edge unless the reader is already
there, so a catch-up cannot yank someone who scrolled up to read.
@ardaerzin
ardaerzin changed the base branch from release/v0.106.1 to release/v0.106.2 July 30, 2026 16:13
@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🧹 Nitpick comments (11)
web/oss/src/components/AgentChatSlice/AgentConversation.tsx (1)

417-427: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider assigning handleSubmitRef from an effect (or memoizing handleSubmit).

Line 427 mutates the ref during render. It happens to be safe here (consumers read it inside effects/events, which run after commit), but a render-phase side effect is fragile if a future consumer reads it during render. useEffect(() => { handleSubmitRef.current = handleSubmit }) makes the contract explicit.

web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts (3)

202-221: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse markLiveGate here instead of writing liveGateInteractionRef directly.

Line 204 duplicates the only other write path (markLiveGate, line 194); routing both through one setter keeps the "live gate" invariant in one place.


307-333: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Full transcript scan on every commit.

This effect re-walks every message and part on each throttled commit (~20×/s while streaming). data-committed-revision only ever arrives on the streaming tail, so scanning the last message (or messages after a cursor index) keeps the hot path O(1) in transcript length.


344-367: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Swallowed control-plane failures leave no trace.

Both killSession (line 359) and commandSessionStream (line 366) discard errors silently, so a failed cancel — where the runner keeps running and billing — is invisible. A console.warn in the catch keeps the UX unchanged while making the failure diagnosable.

🛠️ Proposed change
-                .catch(() => {})
+                .catch((err) => console.warn("[AgentChat] killSession failed:", err))
             return
         }
-        commandSessionStream({sessionId, projectId}).catch(() => {})
+        commandSessionStream({sessionId, projectId}).catch((err) =>
+            console.warn("[AgentChat] cancel command failed:", err),
+        )
web/oss/src/components/AgentChatSlice/components/AgentComposerDock.tsx (3)

259-259: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive one blocked flag instead of re-deriving it per control.

composerDisabled is documented as "IDE hand-off / no model key", which is exactly what onboardingActive ? ideHandoffActive : modelBlocked recomputes at Lines 259 and 303-305. Two sources of truth for the same state will drift; prefer passing (and using) the single flag for input, mic and attach.

Also applies to: 303-305, 327-327


250-396: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider memoizing the prefix/header/trailing JSX slots.

These element trees are rebuilt on every dock render and handed to the lazily-loaded Lexical input as props, defeating any memoization inside it. useMemo per slot (plus a stable onSubmit/onPasteFile via useCallback) keeps the heavy editor from re-rendering on unrelated state changes such as busy or queue updates.

As per coding guidelines, "Memoize inline arrays containing objects or JSX when passing them as props to avoid unnecessary rerenders."

Source: Coding guidelines


37-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Trim the narrative comment blocks.

Several of these blocks are multi-paragraph design rationale rather than surprising-constraint notes (the layout-shift and lazy-ref ordering notes do qualify; the "Owner call" / strip-rhythm prose does not).

As per coding guidelines, "Keep in-code comments to at most one short line; use longer comments only for genuinely surprising constraints such as bugs, races, or ordering requirements."

Also applies to: 169-177, 219-224

Source: Coding guidelines

web/oss/src/components/AgentChatSlice/hooks/useComposerDraft.ts (2)

51-62: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Unmount capture also fires on sessionId change.

The effect is keyed on sessionId, so a session switch within a mounted pane runs this cleanup with the previous sessionId while reading the new editor content — persisting text under the wrong key. If panes are strictly one-per-session (as the doc comment implies) this is unreachable today; a sessionId ref snapshot would make it order-independent.


17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer RefObject<boolean> over React.MutableRefObject.

In React 19's type definitions, ref types are unified under RefObject, making MutableRefObject a deprecated shorthand. Using RefObject also matches richInputRef and removes an unimported React. namespace reference.

♻️ Proposed change
@@
-    revealPlayedRef: React.MutableRefObject<boolean>
+    revealPlayedRef: RefObject<boolean>
web/oss/src/components/AgentChatSlice/hooks/useVoiceComposer.ts (2)

29-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc block is attached to the wrong declaration.

The send-vs-attach rationale sits above voiceEnabled (Line 40), whose own one-liner then follows it; move it onto voiceWillSend/startVoiceMessage where the decision actually lives.


46-73: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Wrap startVoiceMessage/dismissMicError in useCallback.

Both are handed down as props (onStartAudio, onDismiss) from AgentComposerDock; new identities each render block any memoization of the voice controls.

As per coding guidelines, "Minimize React re-renders with useMemo, useCallback, and React.memo where appropriate; avoid unstable inline functions and objects".

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 763bfad5-e5ff-4f7d-99b0-4081a1fe4d77

📥 Commits

Reviewing files that changed from the base of the PR and between a0ef2cc and 3f0cce2.

📒 Files selected for processing (22)
  • web/oss/src/components/AgentChatSlice/AgentConversation.tsx
  • web/oss/src/components/AgentChatSlice/assets/conversationLayout.ts
  • web/oss/src/components/AgentChatSlice/assets/messageParts.ts
  • web/oss/src/components/AgentChatSlice/assets/runError.ts
  • web/oss/src/components/AgentChatSlice/assets/scrollGeometry.ts
  • web/oss/src/components/AgentChatSlice/components/AgentComposerDock.tsx
  • web/oss/src/components/AgentChatSlice/components/AgentTranscript.tsx
  • web/oss/src/components/AgentChatSlice/components/AgentTurn.tsx
  • web/oss/src/components/AgentChatSlice/components/MessageRow.tsx
  • web/oss/src/components/AgentChatSlice/components/TranscriptPlaceholder.tsx
  • web/oss/src/components/AgentChatSlice/components/TurnActivity.tsx
  • web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts
  • web/oss/src/components/AgentChatSlice/hooks/useComposerAttachments.ts
  • web/oss/src/components/AgentChatSlice/hooks/useComposerDraft.ts
  • web/oss/src/components/AgentChatSlice/hooks/useFirstRunSeed.ts
  • web/oss/src/components/AgentChatSlice/hooks/useOnboardingChat.ts
  • web/oss/src/components/AgentChatSlice/hooks/useScrollIntent.ts
  • web/oss/src/components/AgentChatSlice/hooks/useSessionHydration.ts
  • web/oss/src/components/AgentChatSlice/hooks/useTranscriptScroll.ts
  • web/oss/src/components/AgentChatSlice/hooks/useTurnInspector.ts
  • web/oss/src/components/AgentChatSlice/hooks/useVirtuosoTranscript.tsx
  • web/oss/src/components/AgentChatSlice/hooks/useVoiceComposer.ts

Comment on lines +240 to +267
useEffect(() => {
if (!error) return
const parsed = parseAgentRunError(error)
setMessages((prev) => {
const last = prev.length > 0 ? prev[prev.length - 1] : undefined
const existing = (last?.metadata as {runError?: {message?: string}} | undefined)
?.runError
if (last?.role === "assistant") {
if (existing?.message === parsed.message) return prev // already stamped
const next = [...prev]
next[next.length - 1] = {
...last,
metadata: {...(last.metadata as object | undefined), runError: parsed},
}
return next
}
// No trailing assistant turn (failed before one existed) — add a minimal carrier.
return [
...prev,
{
id: `run-error-${generateId()}`,
role: "assistant",
parts: [],
metadata: {runError: parsed},
} as (typeof prev)[number],
]
})
}, [error, setMessages])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@ai-sdk/react useChat error reset on new sendMessage

💡 Result:

In the AI SDK (specifically starting from newer versions), the error state returned by the useChat hook does not automatically reset when you call sendMessage [1][2][3]. If an error occurs, it persists in the state, often requiring explicit handling to clear it so that subsequent interactions can proceed smoothly [1][2][4]. To manage this, you have a few options: 1. Use clearError: Most recent versions of the @ai-sdk/react package provide a clearError function directly from the useChat hook [3][5][6]. You can call this method before or during your submission logic to reset the error state [1][3]. const { sendMessage, error, clearError } = useChat; const handleSubmit = (e) => { e.preventDefault; if (error) clearError; // Clear the error before sending a new message sendMessage({ text: input }); }; 2. Manual State Reset: If you are on an older version that lacks clearError, or if you need custom behavior, you can use the setMessages function to manipulate the message array. A common pattern is to remove the last (failed) message if the assistant stream was interrupted before sending a new request [2][4]. 3. Error Handling Callback: You can also use the onError callback provided in the useChat options to handle side effects when an error occurs, though this does not automatically clear the state for the UI [2][4][7]. If you find that your UI remains stuck or disabled due to the error, ensure that you are checking the error state in your conditional rendering (e.g., to enable/disable input fields or show/hide retry buttons) and explicitly invoking a reset mechanism [2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file and package info =="
git ls-files | rg '(^|/)useAgentChatSession\.ts$|(^|/)package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$' || true

echo
echo "== useAgentChatSession outline/sections =="
if [ -f web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts ]; then
  wc -l web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts
  sed -n '1,340p' web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts | cat -n
fi

echo
echo "== dependency declarations for `@ai-sdk/react` ai package =="
rg -n '"(`@ai-sdk/react`|ai)"|(`@ai-sdk/react`|ai)\@' package.json web/package.json pnpm-lock.yaml web/pnpm-lock.yaml . 2>/dev/null | head -120

Repository: Agenta-AI/agenta

Length of output: 36704


🌐 Web query:

vercel ai useChat clearError reset error new submission @ai-sdk/react 3

💡 Result:

In the Vercel AI SDK (specifically @ai-sdk/react), you can manage error states in the useChat hook using the built-in clearError function [1][2]. The clearError function is available on the object returned by useChat and allows you to programmatically clear any existing error state without needing to trigger a reload or perform a new submission [1][3]. Usage: const { error, clearError } = useChat; // To clear the error: clearError; This is particularly useful when you want to reset the UI or allow the user to try again after a failed attempt without leaving the error message permanently displayed [4]. If you are using an older version of the SDK where clearError is not available, common workarounds include using the regenerate function to attempt a retry, or re-initializing the chat state if necessary [5][6]. For full control, you can also process errors via the onError callback option provided when initializing useChat [5][6].

Citations:


Clear the previous error before submitting again, or track the stamped error separately.

useChat keeps error until it is cleared explicitly, and the tail-based dedupe only prevents overwriting the same message. If the last turn changes to user before error and messages update, this effect can append another run-error-* assistant carrier; call clearError(error) before sendMessage, or store the parsed error identity in a ref and avoid appending while it remains stamped.

Comment on lines +94 to +95
autoStartedSeedRef.current = true
handleSubmitRef.current(firstRunPrompt)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unhandled rejection from the seeded auto-send.

handleSubmitRef.current is typed void | Promise<void> and the concrete handleSubmit in AgentConversation.tsx is async (it awaits filesToParts). A rejection here escapes as an unhandled promise rejection — the same case the codebase already handles via ignoreStreamRejection.

🛡️ Proposed fix
-        handleSubmitRef.current(firstRunPrompt)
+        void Promise.resolve(handleSubmitRef.current(firstRunPrompt)).catch((err) =>
+            console.warn("[AgentChat] first-run seed send failed:", err),
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
autoStartedSeedRef.current = true
handleSubmitRef.current(firstRunPrompt)
autoStartedSeedRef.current = true
void Promise.resolve(handleSubmitRef.current(firstRunPrompt)).catch((err) =>
console.warn("[AgentChat] first-run seed send failed:", err),
)

Comment thread web/oss/src/components/AgentChatSlice/hooks/useOnboardingChat.ts Outdated
Comment thread web/oss/src/components/AgentChatSlice/hooks/useSessionHydration.ts Outdated
Comment thread web/oss/src/components/AgentChatSlice/hooks/useTranscriptScroll.ts Outdated
Review pass on the AgentConversation split. All five predate the split; the
move only made them legible.

- Attachment `uid` was derived from name+mtime+size, so the same file attached
  twice produced two tray rows with one identity — and `uid` is the React key,
  the preview-URL key, and the remove/view/retry handle. Mint it once and
  enqueue the minted value instead of re-deriving the string.
- `filesToParts` used `Promise.all`, so one unreadable file rejected the whole
  batch. Every one of `handleSubmit`'s four call sites drops its promise, so
  that surfaced as an unhandled rejection and a send that silently did nothing.
  Report failures as data (`allSettled`), hold the send, and name the file
  through the inline rejection channel the other refusals already use.
- The IDE-bubble typewriter kept one timer handle, so a second run (Start over
  mid-animation) stranded the first chain past every cleanup. One cancel, called
  on start, on Start over, and on unmount.
- Session hydration adopted the initial result unguarded while its refresh
  sibling was guarded. Route both through one adopter that tracks what it has
  already adopted, so the guard no longer depends on React commit timing.
- SC-4 follow-release bound listeners to whatever scroll node existed at mount.
  Toggling virtualization off remounts that container, leaving the handlers on a
  detached node for the rest of the session. Track the node as state and key the
  node-bound effects on it; `attachScroll` is now the only way to attach it.
@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
web/oss/src/components/AgentChatSlice/components/AgentTranscript.tsx (2)

40-43: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Route the jump button through the mounted transcript engine.

useVirtuoso derives only from virt.enabled, but the Virtuoso branch renders only when messages.length > 0. When the native empty-state fallback mounts, virt.jumpToLatest still has no mounted Virtuoso scroller to scroll, and the native handler is skipped because useVirtuoso is true. Derive the handler and any engine-specific logic from the same mounted-engine condition, or clear showJump before the fallback mounts.


40-99: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add live-region semantics to the virtualized transcript.

The <Virtuoso> branch replaces the native container that has role="log", aria-live="polite", and aria-label. virt.components.Scroller is a no-op placeholder and does not add those attributes. Implement the custom Scroller with the equivalent role="log", aria-live="polite", and aria-label="Agent conversation" so the virtualized transcript exposes the same live-region behavior.

web/oss/src/components/AgentChatSlice/hooks/useTranscriptScroll.ts (1)

95-140: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cancel interrupted pins instead of settling them as successful.

animatePinTo registers scrollend and a 700 ms fallback listener, then calls onSettle from them. A later scrollend from user input can run after the pin was overridden by the user, which sets stickRef.current = true and jumps back to the bottom. The fixed timer can also settle a long native glide before it reaches the target. Cancel on all user-scroll inputs and settle only after confirming the target position; keep the timer as a true fallback when scrollend is unavailable.

Also applies to lines 182-195 and 261-285.

🧹 Nitpick comments (1)
web/oss/src/components/AgentChatSlice/components/AgentTranscript.tsx (1)

44-99: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Stabilize Virtuoso props during streaming.

itemContent at Line 73 and the increaseViewportBy, style, and context objects at Lines 49-97 are recreated whenever AgentTranscript renders. AgentConversation supplies a live messages stream, so this path renders frequently.

Memoize callbacks and stable objects with their true dependencies. Isolate the changing active footer so it does not invalidate unrelated Virtuoso props. Verify that active footer updates remain visible.

As per coding guidelines: “Minimize React re-renders with useMemo, useCallback, and React.memo where appropriate; avoid unstable inline functions and objects, especially in lists.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6593aa16-1176-41a0-8581-9c646adad3f7

📥 Commits

Reviewing files that changed from the base of the PR and between 3f0cce2 and 01f11b7.

📒 Files selected for processing (7)
  • web/oss/src/components/AgentChatSlice/AgentConversation.tsx
  • web/oss/src/components/AgentChatSlice/assets/files.ts
  • web/oss/src/components/AgentChatSlice/components/AgentTranscript.tsx
  • web/oss/src/components/AgentChatSlice/hooks/useComposerAttachments.ts
  • web/oss/src/components/AgentChatSlice/hooks/useOnboardingChat.ts
  • web/oss/src/components/AgentChatSlice/hooks/useSessionHydration.ts
  • web/oss/src/components/AgentChatSlice/hooks/useTranscriptScroll.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • web/oss/src/components/AgentChatSlice/hooks/useSessionHydration.ts
  • web/oss/src/components/AgentChatSlice/hooks/useComposerAttachments.ts
  • web/oss/src/components/AgentChatSlice/hooks/useOnboardingChat.ts

Comment thread web/oss/src/components/AgentChatSlice/hooks/useTranscriptScroll.ts
…remounts

The scroll container can be replaced within one session (virtualization toggled,
or an empty conversation rendering the plain node under Virtuoso). Three pieces
of state are measured against whichever node was live and none of them were
reset on the swap:

- the scroll baseline, so the first scroll-down-to-edge on the new node compared
  against the old node's scrollTop and could fail `scrollTop > prevTop` — follow
  then stayed off at the bottom until the next scroll,
- the SC-3 anchor, whose offset was taken in the old node's coordinate space,
- an in-flight glide, still holding listeners on the node that went away.

Clear all three where the node changes. The anchor is dropped rather than
recaptured: its offset has no meaning in the new container, and the next scroll
or pointer-down re-anchors.
@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

// old node's coordinate space), and any glide still animating the node that just went away.
// Runs before the SC-1/SC-2 pin below, which is declared later.
useLayoutEffect(() => {
pinCleanupRef.current?.()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 The AI agent says:

[P2] Release the scroll guard when cancelling a node pin

When the plain scroll node is replaced while animatePinTo is active, such as when virtualization is toggled during a submit or jump glide, this cleanup removes the pin listeners and timer but leaves programmaticScrollRef.current set to true. If the plain engine mounts again, its scroll handler, resize observer, and follow effect continue treating every scroll as programmatic, disabling normal autoscroll and anchoring until another pin happens to settle.

Clear the guard or re-arm the pin against the replacement node when cancelling for a node swap.

mmabrouk and others added 6 commits July 31, 2026 20:01
…session-multi-tab

Resolves the useSessionHydration.ts conflict between the watermark-based
adoption rework (#5589) and the latent-defect review pass (01f11b7 on
#5569) so both intents survive:

- Keep #5589's single `adoptServerTranscript` adopter (record-count
  watermark via `shouldAdoptServerTranscript`) for the hydration path,
  the SWR revalidation path, and the remote-run poll.
- Port 01f11b7's hydration fix into that structure: a local `adopted`
  flag so a stale empty first result cannot set the "history unavailable"
  notice after the background refetch already adopted real history.
- The rest of 01f11b7's commit-timing guard (local high-water mark /
  adopted snapshot) is subsumed by the watermark ref, which the adopter
  updates synchronously before any React commit; documented that
  invariant on the write so it is not lost.
- The count-based guard and paused-tail special case from the old SWR
  effect are intentionally gone — #5589 replaced them with the watermark
  ("the special case is gone rather than extended").
`useScrollIntent` returns a fresh object every render, and `intent` sat in
`adoptServerTranscript`'s dependency list — so the callback was recreated on
every render, and the remote-run poll effect (keyed on it) tore down and
re-armed its timer each time. The liveness query alone re-renders the hook
~every 15s while a run is live elsewhere, i.e. on the same cadence as the
poll it was cancelling: the record-log catch-up could be starved for the
whole run and its backoff never accumulated.

Two layers, so the timer's stability is true by construction, not by audit:
depend on `intent.armJump` / `intent.stickRef` (both stable) instead of the
per-render `intent` object, and have the poll effect read the current
adopter through a ref, keyed only on `runningElsewhere` + `sessionId`.
… skipped

When even the active session alone overflows the localStorage quota,
`writeMessagesWithQuotaGuard` skips the transcript write — but
`persistSessionMessagesAtom` still wrote the NEW `recordCount`. After a
reload that pairs the OLD (or missing) cached messages with the newer
watermark, so `shouldAdoptServerTranscript` rejects the complete server log
as "not newer" on every open and the stale cache is frozen until the log
happens to outgrow the bogus number.

Surface `persisted` from the quota guard and drop the session's watermark
when the write was skipped, enforcing the invariant already documented on
`dropSessionMessages`: the transcript store and the watermark store must
never diverge.
[fix] Release the scroll guard when a transcript node swap cancels a pin
[fix] Rebase #5589 onto #5569 and fix the two blocking review findings
[fix] Stop agent sessions breaking when open in two places [AGE-4004]
@mmabrouk
mmabrouk merged commit 940d2f2 into release/v0.106.2 Jul 31, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Frontend refactoring A code change that neither fixes a bug nor adds a feature size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants