Skip to content

feat(agents): restore the per-pane agent selector in the pane bar - #2276

Merged
2witstudios merged 1 commit into
masterfrom
pu/pane-agent-selector
Jul 30, 2026
Merged

2witstudios merged 1 commit into
masterfrom
pu/pane-agent-selector

Conversation

@2witstudios

Copy link
Copy Markdown
Owner

Summary

Restores the /development AISelector as the pane bar's chat-pane identity — an audit follow-up from PR #2258/#2259 (Dev → Agents Rebuild). The old machine grid's MachinePaneChat hosted an AISelector dropdown as the pane bar's own identity (selectedAgent={pane.selectedAgent} onSelectAgent={pane.selectAgent}, disabled while streaming), so a pane's agent could be switched at any time. The pane-system restoration ported the grid but replaced the bar's identity with a static conversation name — the only remaining agent choice was the PanePicker, reachable only via Split on an unbound pane.

Zero data-model impact. Panes are local, unaddressed UI state; a session already owns many conversations with any of its drive's agents. Switching a pane's agent never rebinds a conversation — binding stays congenital and permanent — it just points the pane at a different conversation in the same session (minting one lazily if none exists yet), exactly the semantics the historical code documented: "returning to null resumes the conversation as-is, it never mints a new row." No schema change, no new API route.

  • select-pane-agent.ts (new, pure module + test): the switch decision — no-op when the pick matches the pane's current agent, focus the session's most-recently-active conversation with the picked agent, or mint when none exists. Mirrors useAgentWorkspaceStore's openConversation focus-or-open policy, scoped to one specific pane.
  • AISelector.tsx: adds an optional pre-scoped agents list and a canPickAssistant flag, both additive and backward-compatible (existing callers — GlobalAssistantView, SidebarChatTab — are unaffected). driveId alone can't express "no drive at all": an absent driveId means "every drive" to the internal usePageAgents fetch, which would leak cross-drive agents into a global-assistant session's pane (a session that owns no drive to pick agents from). A session-scoped caller supplies its own already-correctly-scoped list instead.
  • AgentPanes.tsx: chat panes now render the selector in the pane bar identity slot, fed by the same pickableAgents already computed for the split picker (so the bar and the picker never disagree about what's choosable). Switching reuses the store's dedup-aware openConversation for an existing thread, or the picker's own mint path (handlePickAgent) for a fresh one. Disabled while the pane's chat is streaming or mid-mint. The session's conversation list rides the same bulk /api/agent-sessions listing the sidebar already polls (SWR dedupes the shared key) — no new endpoint.

Side benefit: the bar now displays which agent each pane is talking to, closing the "which agent is this pane" gap the static conversation name left open.

Test plan

  • bun run typecheck (monorepo, via turbo)
  • bun run lint (monorepo, via turbo) — only pre-existing warnings in unrelated files
  • bun run test:unit (monorepo) — all green except one pre-existing, unrelated, timezone-dependent flake (src/lib/messages/__tests__/grouping.test.ts, untouched by this branch)
  • bun run knip:check — within baseline
  • New/updated suites: select-pane-agent.test.ts, AISelector.test.tsx (agents override + canPickAssistant), AgentPanes.test.tsx (selector renders current agent, switch focuses an existing conversation, switch mints when none exists, disabled while streaming, no-op on same agent)
  • Manual: bun run dev → Agents console Chat tab → pane bar shows the agent dropdown → switch to another drive agent → pane swaps to that agent's conversation (existing one focused if present, else a new thread appears in the sidebar under the same session); terminal pane bar unchanged; selector disabled mid-stream

🤖 Generated with Claude Code

https://claude.ai/code/session_01DfWmmVg3RqktRfAHtzEikF

Restores the /development AISelector as the pane bar's chat-pane
identity — a follow-up from the adversarial audit of PR #2258/#2259
(Dev → Agents Rebuild): the old machine grid could switch a pane's
agent at any time via a dropdown in the bar; the pane-system
restoration replaced that with a static conversation name, leaving
the picker (Split-only) as the sole way to choose an agent.

Zero data-model impact: panes are local, unaddressed UI state, and a
session already owns many conversations with any of its drive's
agents. Switching a pane's agent never rebinds a conversation
(binding stays congenital and permanent) — it points the pane at a
different conversation in the same session, focusing an existing one
or minting a fresh one lazily. No schema change, no new API route.

- select-pane-agent.ts: pure focus-or-mint decision (noop when the
  pick matches the pane's current agent, focus the session's most
  recently active conversation with that agent, or mint), with its
  own test file per the project's branching rule.
- AISelector.tsx: accepts an optional pre-scoped `agents` list and a
  `canPickAssistant` flag. `driveId` alone can't express "no drive" —
  an absent driveId means "every drive" to usePageAgents — so a
  session-scoped pane (which may own no drive at all, in the
  global-assistant case) supplies its own list instead of the
  internal cross-drive fetch.
- AgentPanes.tsx: chat panes render the selector, fed by the same
  pickableAgents already computed for the split picker; switching
  reuses the store's dedup-aware openConversation for an existing
  thread, or the picker's own mint path for a new one. Disabled while
  the pane's chat is streaming.

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

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

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{ revalidateOnFocus: false, refreshInterval: 20_000 },
);
const sessionConversations: SessionConversationSummary[] = useMemo(
() => (sessionsData?.sessions ?? []).find((session) => session.sessionId === sessionId)?.conversations ?? [],

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 the selector disabled until conversations load

On a cold mount or while this request is retrying, sessionsData is undefined and this normalization treats it as an authoritative empty conversation list. If the agent options are already cached, a user can use the selector before the session listing completes; selecting an agent that already has a thread then takes the mint path and persists a duplicate instead of resuming it. Preserve the loading/error distinction and disable switching or fetch on demand until the conversation list is available.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch — fixed in #2278: the selector now stays disabled until sessionConversations has loaded at least once (tracked via the SWR hook's own isLoading), same treatment as the existing mid-mint/mid-stream disable. This PR (#2276) merged before this review finished, so the fix landed as a follow-up rather than a push here.

});
return;
}
void handlePickAgent(paneId, nextAgentPageId);

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 Refresh the session list after minting a conversation

After this selector-driven mint succeeds, only the pane store is updated; sessionConversations still lacks the new row until the 20-second SWR refresh. In a one-pane session, switching from newly minted agent B back to existing agent A and then to B again within that interval reaches this handlePickAgent call a second time and persists another B conversation. Optimistically add the new summary or mutate/revalidate the session-list cache before allowing another selection.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch — fixed in #2278: a successful mint now writes the new conversation straight into the sessionsData SWR cache locally (mutate(..., { revalidate: false })), so a switch-back-then-forward inside the 20s poll window resumes it instead of minting a duplicate. This PR (#2276) merged before this review finished, so the fix landed as a follow-up rather than a push here.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@2witstudios, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: abe17ff9-87ae-42f8-9450-6fff2468d7d3

📥 Commits

Reviewing files that changed from the base of the PR and between 900bc25 and cea414a.

📒 Files selected for processing (6)
  • apps/web/src/components/agents/panes/AgentPanes.tsx
  • apps/web/src/components/agents/panes/__tests__/AgentPanes.test.tsx
  • apps/web/src/components/agents/panes/__tests__/select-pane-agent.test.ts
  • apps/web/src/components/agents/panes/select-pane-agent.ts
  • apps/web/src/components/ai/shared/AISelector.tsx
  • apps/web/src/components/ai/shared/__tests__/AISelector.test.tsx

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.

2witstudios added a commit that referenced this pull request Jul 30, 2026
Restores the missing level in the pane grid's model: session ->
conversation (agent listing) -> panes. Previously a session's pane grid
was keyed per-session only, so closing the last pane bound to one
agent's conversation was a pure layout act — its row stayed in the
sidebar forever (and held a conversation-cap slot) unless the grid's
actual last pane was closed, which ended the whole session instead.

- packages/db/schema/conversations.ts: additive nullable
  closedInSessionAt timestamp, deliberately separate from isActive
  (history soft-delete) — closing a listing never touches history.
- lib/agent-sessions/close-conversation-in-session.ts: pure decision
  module (closed | already_closed | not_in_session | last_conversation),
  fail-closed on the never-empty invariant. Wired in
  agent-sessions-runtime.ts with a per-session transaction + advisory
  lock (the #2272 createIfUnderLimit pattern) so two racing closes of
  the last two listings serialize.
- New DELETE /api/agent-sessions/[sessionId]/conversations/[conversationId]
  route, session-scoped and uniform for page-agent + assistant threads.
- components/agents/panes/close-pane.ts: pure client verdict
  (noop | close-pane | close-conversation | end-session) covering
  duplicate-view panes, an unloaded/stale listing, grid-last rebind to
  the most recently active other conversation, and ending the session
  only when its LAST open listing closes (even with terminal panes
  still in the grid).
- AgentPanes.tsx wired to the verdict; cleanupOrphanedConversation now
  uses the session-scoped route (fixes a pre-existing defect where an
  orphaned mid-mint conversation stayed listed and held a cap slot
  forever). AgentsSurface/AgentPageView follow a close's rebind for
  their own independent "current conversation" tracking.

Coordination: PR #2276 (pu/pane-agent-selector) also adds a sessions-
listing SWR fetch and a pure panes/ module (select-pane-agent.ts,
deciding *switch*; this PR's close-pane.ts decides *close* — no logical
conflict). Whichever lands second should dedupe the listing fetch and
share a SessionConversationSummary type.

This fixes a directly-assigned bug report, not a filed GitHub issue —
there is no issue number for this PR to close.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5FJqcnWyZXBmuc9iTFd6U
@2witstudios
2witstudios merged commit e672903 into master Jul 30, 2026
3 checks passed
2witstudios added a commit that referenced this pull request Jul 30, 2026
Restores the missing level in the pane grid's model: session ->
conversation (agent listing) -> panes. Previously a session's pane grid
was keyed per-session only, so closing the last pane bound to one
agent's conversation was a pure layout act — its row stayed in the
sidebar forever (and held a conversation-cap slot) unless the grid's
actual last pane was closed, which ended the whole session instead.

- packages/db/schema/conversations.ts: additive nullable
  closedInSessionAt timestamp, deliberately separate from isActive
  (history soft-delete) — closing a listing never touches history.
- lib/agent-sessions/close-conversation-in-session.ts: pure decision
  module (closed | already_closed | not_in_session | last_conversation),
  fail-closed on the never-empty invariant. Wired in
  agent-sessions-runtime.ts with a per-session transaction + advisory
  lock (the #2272 createIfUnderLimit pattern) so two racing closes of
  the last two listings serialize.
- New DELETE /api/agent-sessions/[sessionId]/conversations/[conversationId]
  route, session-scoped and uniform for page-agent + assistant threads.
- components/agents/panes/close-pane.ts: pure client verdict
  (noop | close-pane | close-conversation | end-session) covering
  duplicate-view panes, an unloaded/stale listing, grid-last rebind to
  the most recently active other conversation, and ending the session
  only when its LAST open listing closes (even with terminal panes
  still in the grid).
- AgentPanes.tsx wired to the verdict; cleanupOrphanedConversation now
  uses the session-scoped route (fixes a pre-existing defect where an
  orphaned mid-mint conversation stayed listed and held a cap slot
  forever). AgentsSurface/AgentPageView follow a close's rebind for
  their own independent "current conversation" tracking.

Coordination: PR #2276 (pu/pane-agent-selector) also adds a sessions-
listing SWR fetch and a pure panes/ module (select-pane-agent.ts,
deciding *switch*; this PR's close-pane.ts decides *close* — no logical
conflict). Whichever lands second should dedupe the listing fetch and
share a SessionConversationSummary type.

This fixes a directly-assigned bug report, not a filed GitHub issue —
there is no issue number for this PR to close.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5FJqcnWyZXBmuc9iTFd6U
2witstudios added a commit that referenced this pull request Jul 30, 2026
…owup

fix(agents): close two pane-selector mint races flagged in #2276 review
2witstudios added a commit that referenced this pull request Jul 30, 2026
Restores the missing level in the pane grid's model: session ->
conversation (agent listing) -> panes. Previously a session's pane grid
was keyed per-session only, so closing the last pane bound to one
agent's conversation was a pure layout act — its row stayed in the
sidebar forever (and held a conversation-cap slot) unless the grid's
actual last pane was closed, which ended the whole session instead.

- packages/db/schema/conversations.ts: additive nullable
  closedInSessionAt timestamp, deliberately separate from isActive
  (history soft-delete) — closing a listing never touches history.
- lib/agent-sessions/close-conversation-in-session.ts: pure decision
  module (closed | already_closed | not_in_session | last_conversation),
  fail-closed on the never-empty invariant. Wired in
  agent-sessions-runtime.ts with a per-session transaction + advisory
  lock (the #2272 createIfUnderLimit pattern) so two racing closes of
  the last two listings serialize.
- New DELETE /api/agent-sessions/[sessionId]/conversations/[conversationId]
  route, session-scoped and uniform for page-agent + assistant threads.
- components/agents/panes/close-pane.ts: pure client verdict
  (noop | close-pane | close-conversation | end-session) covering
  duplicate-view panes, an unloaded/stale listing, grid-last rebind to
  the most recently active other conversation, and ending the session
  only when its LAST open listing closes (even with terminal panes
  still in the grid).
- AgentPanes.tsx wired to the verdict; cleanupOrphanedConversation now
  uses the session-scoped route (fixes a pre-existing defect where an
  orphaned mid-mint conversation stayed listed and held a cap slot
  forever). AgentsSurface/AgentPageView follow a close's rebind for
  their own independent "current conversation" tracking.

Coordination: PR #2276 (pu/pane-agent-selector) also adds a sessions-
listing SWR fetch and a pure panes/ module (select-pane-agent.ts,
deciding *switch*; this PR's close-pane.ts decides *close* — no logical
conflict). Whichever lands second should dedupe the listing fetch and
share a SessionConversationSummary type.

This fixes a directly-assigned bug report, not a filed GitHub issue —
there is no issue number for this PR to close.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5FJqcnWyZXBmuc9iTFd6U
2witstudios added a commit that referenced this pull request Jul 30, 2026
Restores the missing level in the pane grid's model: session ->
conversation (agent listing) -> panes. Previously a session's pane grid
was keyed per-session only, so closing the last pane bound to one
agent's conversation was a pure layout act — its row stayed in the
sidebar forever (and held a conversation-cap slot) unless the grid's
actual last pane was closed, which ended the whole session instead.

- packages/db/schema/conversations.ts: additive nullable
  closedInSessionAt timestamp, deliberately separate from isActive
  (history soft-delete) — closing a listing never touches history.
- lib/agent-sessions/close-conversation-in-session.ts: pure decision
  module (closed | already_closed | not_in_session | last_conversation),
  fail-closed on the never-empty invariant. Wired in
  agent-sessions-runtime.ts with a per-session transaction + advisory
  lock (the #2272 createIfUnderLimit pattern) so two racing closes of
  the last two listings serialize.
- New DELETE /api/agent-sessions/[sessionId]/conversations/[conversationId]
  route, session-scoped and uniform for page-agent + assistant threads.
- components/agents/panes/close-pane.ts: pure client verdict
  (noop | close-pane | close-conversation | end-session) covering
  duplicate-view panes, an unloaded/stale listing, grid-last rebind to
  the most recently active other conversation, and ending the session
  only when its LAST open listing closes (even with terminal panes
  still in the grid).
- AgentPanes.tsx wired to the verdict; cleanupOrphanedConversation now
  uses the session-scoped route (fixes a pre-existing defect where an
  orphaned mid-mint conversation stayed listed and held a cap slot
  forever). AgentsSurface/AgentPageView follow a close's rebind for
  their own independent "current conversation" tracking.

Coordination: PR #2276 (pu/pane-agent-selector) also adds a sessions-
listing SWR fetch and a pure panes/ module (select-pane-agent.ts,
deciding *switch*; this PR's close-pane.ts decides *close* — no logical
conflict). Whichever lands second should dedupe the listing fetch and
share a SessionConversationSummary type.

This fixes a directly-assigned bug report, not a filed GitHub issue —
there is no issue number for this PR to close.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5FJqcnWyZXBmuc9iTFd6U
@2witstudios
2witstudios deleted the pu/pane-agent-selector branch July 31, 2026 01:19
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.

1 participant