Skip to content

fix(agents): show shells the agent opens or closes - #2256

Closed
2witstudios wants to merge 1 commit into
masterfrom
pu/agent-shell-revalidate
Closed

2witstudios wants to merge 1 commit into
masterfrom
pu/agent-shell-revalidate

Conversation

@2witstudios

@2witstudios 2witstudios commented Jul 29, 2026 •

Copy link
Copy Markdown
Owner

Fixes #2255. Independent of #2253 — different files, merges in either order.

The bug

spawn_shell and kill_shell write agent_session_shells server-side, never through useSessionShells's addShell/removeShell — the only things that update the cache holding the tab list. That SWR entry has:

{ revalidateOnFocus: false, dedupingInterval: 5_000 }   // no polling, no socket invalidation

and there is no event to listen for either: every shell socket event (shell:output, shell:ready, shell:closed, shell:error) is connection-tagged rather than broadcast, deliberately — the rebuild plan states "No rooms.ts change needed."

Net effect: an agent-opened shell never appears as a tab, and an agent-closed one never disappears, until an unrelated revalidation or a remount happens to fix it.

The fix

End-of-turn is the one moment the client knows the agent may have acted, which makes it the honest place to re-read anything the agent could have changed. SessionChat fires onTurnComplete on the streaming → idle edge; AgentView wires it to shells.mutate. One GET per completed turn, only while the view is mounted.

The edge, not the level. Notifying whenever displayIsStreaming is false would fire on its resting value — every render, forever, for a turn that never ran. The previous value lives in a ref so observing the transition cannot itself cause a render. The callback's identity is unstable (mutate is a fresh arrow each render), which is harmless here: the effect re-runs but only fires on a genuine transition.

Why not the broadcast

#2255 lists three options. A shell-lifecycle broadcast from realtime is more correct — but it is the only option that adds a room/broadcast concept to a surface built without one. This is the smallest change that fixes the reported symptom; the broadcast stays available as an upgrade.

What this deliberately does not cover, since a reviewer would find it and it should be stated up front: displayIsStreaming is own-stream only —

const displayIsStreaming =
  activeStream?.isOwn === true ||
  (pendingSendConversationId !== null && pendingSendConversationId === conversationId);

so this fires for my turn, not for another user's turn in a shared conversation, and not for a second browser tab of my own. Both are the broadcast case. The reported symptom — "the agent I am talking to opened a shell and no tab appeared" — is the own-turn case, which this fixes completely.

The coarseness in the other direction is deliberate and cheap: the re-read fires even when no shell tool ran.

Conversation switching is safe: AgentsSurface renders <AgentView key={selectedConversationId}>, so the edge-tracking ref resets on remount rather than carrying a stale "was streaming" across conversations.

Validation

  • typecheck and lint clean across the monorepo.
  • apps/web agents components: 121 tests pass.
  • New tests: fires once on the edge · never on mount while idle · not again on later idle renders · optional for surfaces that don't pass it · AgentView actually passes it to the shells cache.
  • Mutation-verified: deleting the onTurnComplete={shells.mutate} wiring fails the AgentView test and nothing else. That test captures the prop through the SessionChat mock, because a mock that swallowed it would let the wiring be deleted with every test still green — which is the shape of the original bug.

🤖 Generated with Claude Code

https://claude.ai/code/session_017V3eqwRX5cFy3Tdu2Syoro

Summary by CodeRabbit

  • New Features

    • Shell lists now refresh automatically when an agent chat turn finishes.
    • Chat sessions support completion callbacks after streamed responses become idle.
  • Bug Fixes

    • Improved synchronization between completed chat turns and the displayed shell list.
  • Tests

    • Added coverage for callback behavior across streaming, idle, and repeated render states.

`spawn_shell` and `kill_shell` write `agent_session_shells` server-side and
never pass through this view's add/remove handlers, which are the only
things that update the shells SWR entry. That entry has no polling, no
socket invalidation, and focus revalidation explicitly disabled — and
there is no event to listen for either, since every shell socket event is
connection-tagged rather than broadcast, by design. So an agent-opened
shell never became a tab and an agent-closed one never went away, until
some unrelated revalidation or a remount happened to fix it.

End-of-turn is the one moment the client knows the agent may have acted,
so that is where the re-read goes: `SessionChat` fires `onTurnComplete`
on the streaming → idle EDGE, and `AgentView` wires it to the shells
cache. Costs one GET per completed turn, only while the view is mounted.

The edge, not the level: notifying whenever `displayIsStreaming` is false
would fire on its resting value — every render, forever, for a turn that
never ran. The previous value lives in a ref so observing the transition
cannot itself cause a render.

Chose this over a shell-lifecycle broadcast (issue #2255 option 3), which
is more correct and would also cover a second browser tab, but is the only
option that adds a room/broadcast concept to a surface deliberately built
without one. That remains the upgrade path if multi-tab matters.

Tests: fires once on the edge; never on mount while idle; not again on
later idle renders; optional for surfaces that don't care; and AgentView
actually passes it to the shells cache. Mutation-verified — deleting the
`onTurnComplete={shells.mutate}` wiring fails the AgentView test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017V3eqwRX5cFy3Tdu2Syoro
@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

SessionChat now signals when streaming ends, and AgentView uses that signal to revalidate the shell list. Tests cover transition detection, optional callbacks, and shell refresh invocation.

Changes

Shell lifecycle revalidation

Layer / File(s) Summary
SessionChat turn completion
apps/web/src/components/agents/chat/SessionChat.tsx, apps/web/src/components/agents/chat/__tests__/SessionChat.test.tsx
SessionChat invokes an optional callback once when displayIsStreaming changes from true to false, with tests covering idle mounts, repeated idle renders, and omitted callbacks.
AgentView shell refresh
apps/web/src/components/agents/AgentView.tsx, apps/web/src/components/agents/__tests__/AgentView.test.tsx
AgentView passes shells.mutate to SessionChat and verifies that turn completion refreshes the shell list once.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant SessionChat
  participant AgentView
  participant ShellList
  Agent->>SessionChat: Complete streamed turn
  SessionChat->>AgentView: Invoke onTurnComplete
  AgentView->>ShellList: shells.mutate()
  ShellList-->>AgentView: Refresh shell tabs
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the requested revalidation on agent turn completion and covers the new callback wiring with tests.
Out of Scope Changes check ✅ Passed The changes stay focused on shell tab invalidation, related wiring, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main fix: keeping agent shell tabs in sync when shells are opened or closed.
✨ 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 pu/agent-shell-revalidate

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.

@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: 1

🤖 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 `@apps/web/src/components/agents/__tests__/AgentView.test.tsx`:
- Line 59: Remove the duplicate sessionChatProps declaration in the AgentView
test, keeping a single hoisted sessionChatProps definition in the shared scope
so the test compiles without changing its behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f2d9d1bc-7f6d-49f4-b720-83ecc7fe06f0

📥 Commits

Reviewing files that changed from the base of the PR and between 5cd8ce5 and b0346f0.

📒 Files selected for processing (4)
  • apps/web/src/components/agents/AgentView.tsx
  • apps/web/src/components/agents/__tests__/AgentView.test.tsx
  • apps/web/src/components/agents/chat/SessionChat.tsx
  • apps/web/src/components/agents/chat/__tests__/SessionChat.test.tsx

// Captures `onTurnComplete` so the wiring can be exercised: the prop is the
// only thing that tells this view an AGENT changed the shell set, and a mock
// that swallowed it would let the wiring be deleted with every test still green.
const sessionChatProps = vi.hoisted(() => ({ current: {} as { onTurnComplete?: () => void } }));

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicate sessionChatProps declaration.

Line 59 appears twice in the same scope, causing a duplicate identifier compile error and blocking the test suite.

Proposed fix
 const sessionChatProps = vi.hoisted(() => ({ current: {} as { onTurnComplete?: () => void } }));
-const sessionChatProps = vi.hoisted(() => ({ current: {} as { onTurnComplete?: () => void } }));
📝 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
const sessionChatProps = vi.hoisted(() => ({ current: {} as { onTurnComplete?: () => void } }));
const sessionChatProps = vi.hoisted(() => ({ current: {} as { onTurnComplete?: () => void } }));
🤖 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 `@apps/web/src/components/agents/__tests__/AgentView.test.tsx` at line 59,
Remove the duplicate sessionChatProps declaration in the AgentView test, keeping
a single hoisted sessionChatProps definition in the shared scope so the test
compiles without changing its behavior.

@2witstudios

Copy link
Copy Markdown
Owner Author

Superseded by the pane-grid restoration on pu/agents-restore-panes: shells are no longer tabs that need SWR invalidation — they live in panes bound at open time, and the tab surface this patched (AgentView + useSessionShells) has been deleted. The onTurnComplete revalidation seam this PR extended is gone with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent-created shells never appear as tabs (no invalidation for spawn_shell/kill_shell)

1 participant