Repository navigation
Join an existing workspace from the broker status page - #389
Conversation
Adds a "Close this workspace and join new one" control on the Agent Relay Status page: pastes a workspace key from another Pear instance, tears down this project's broker session, and restarts it pinned to that key so this Pear runs as a node on the existing workspace instead of creating its own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 29 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a feature allowing users to close their current broker session and join an existing workspace by providing its workspace key. This is implemented by adding a joinWorkspace method to the BrokerManager, registering a corresponding IPC handler, exposing it to the renderer, and adding a new JoinWorkspaceControl UI component to the BrokerDetailsPage. Feedback on the changes suggests a UX improvement to auto-close the join workspace dialog upon success and clear stale error messages when reopening or typing.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const [open, setOpen] = useState(false) | ||
| const [key, setKey] = useState('') | ||
|
|
||
| if (!project) return null | ||
|
|
||
| const submit = (): void => { | ||
| const trimmed = key.trim() | ||
| if (!trimmed) return | ||
| onJoin(project, trimmed) | ||
| } | ||
|
|
||
| if (!open) { | ||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={() => setOpen(true)} | ||
| className="inline-flex items-center gap-2 rounded-md border border-[var(--pear-border-subtle)] px-3 py-1.5 text-sm text-[var(--pear-text-dim)] hover:border-[var(--pear-accent-dim)] hover:bg-[var(--pear-bg-surface-hover)] hover:text-[var(--pear-text)]" | ||
| > | ||
| <LogOut size={14} /> | ||
| <span>Close this workspace and join new one</span> | ||
| </button> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <div className="rounded-lg border border-[var(--pear-border-subtle)] bg-[var(--pear-bg-raised)] p-3"> | ||
| <p className="mb-2 text-[11px] text-[var(--pear-text-faint)]"> | ||
| Paste the workspace key from another Pear instance's Agent Relay Status page. This closes the | ||
| current broker session for this project and reconnects it to that workspace. | ||
| </p> | ||
| <div className="flex flex-wrap items-center gap-2"> | ||
| <input | ||
| type="text" | ||
| value={key} | ||
| onChange={(event) => setKey(event.target.value)} | ||
| placeholder="Workspace key" | ||
| disabled={joining} | ||
| className="min-w-0 flex-1 rounded-md border border-[var(--pear-border-subtle)] bg-[var(--pear-bg)] px-2 py-1.5 font-mono text-[12px] text-[var(--pear-text)] outline-none focus:border-[var(--pear-accent-dim)]" | ||
| /> | ||
| <button | ||
| type="button" | ||
| onClick={submit} | ||
| disabled={joining || !key.trim()} | ||
| className="inline-flex items-center gap-2 rounded-md border border-[var(--pear-accent-dim)] bg-[var(--pear-accent)]/10 px-3 py-1.5 text-sm text-[var(--pear-accent-bright)] hover:bg-[var(--pear-accent)]/15 disabled:cursor-wait disabled:opacity-60" | ||
| > | ||
| {joining ? <RefreshCw size={14} className="animate-spin" /> : null} | ||
| <span>{joining ? 'Joining' : 'Join workspace'}</span> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => { | ||
| setOpen(false) | ||
| setKey('') | ||
| }} | ||
| disabled={joining} | ||
| className="rounded-md border border-[var(--pear-border-subtle)] px-3 py-1.5 text-sm text-[var(--pear-text-dim)] hover:bg-[var(--pear-bg-surface-hover)]" | ||
| > | ||
| Cancel | ||
| </button> | ||
| </div> | ||
| {joinError && <p className="mt-2 text-sm text-[var(--pear-red)]">{joinError}</p>} | ||
| </div> | ||
| ) |
There was a problem hiding this comment.
UX Improvement: Auto-close Dialog and Clear Stale Errors
Currently, the JoinWorkspaceControl has two usability issues:
- Dialog remains open on success: When a workspace is successfully joined, the dialog stays open with the typed key and the "Join workspace" button enabled, which can confuse users into thinking the action failed or is still pending.
- Stale errors persist: If a join attempt fails and the user cancels/closes the dialog, reopening it later will still display the old error message.
We can resolve both issues cleanly by introducing a local localError state and synchronizing the dialog's open/closed state with the joining and joinError props using useEffect hooks.
const [open, setOpen] = useState(false)
const [key, setKey] = useState('')
const [localError, setLocalError] = useState<string | undefined>(undefined)
useEffect(() => {
setLocalError(joinError)
}, [joinError])
useEffect(() => {
if (!joining && !joinError) {
setOpen(false)
setKey('')
}
}, [joining, joinError])
if (!project) return null
const submit = (): void => {
const trimmed = key.trim()
if (!trimmed) return
onJoin(project, trimmed)
}
if (!open) {
return (
<button
type="button"
onClick={() => setOpen(true)}
className="inline-flex items-center gap-2 rounded-md border border-[var(--pear-border-subtle)] px-3 py-1.5 text-sm text-[var(--pear-text-dim)] hover:border-[var(--pear-accent-dim)] hover:bg-[var(--pear-bg-surface-hover)] hover:text-[var(--pear-text)]"
>
<LogOut size={14} />
<span>Close this workspace and join new one</span>
</button>
)
}
return (
<div className="rounded-lg border border-[var(--pear-border-subtle)] bg-[var(--pear-bg-raised)] p-3">
<p className="mb-2 text-[11px] text-[var(--pear-text-faint)]">
Paste the workspace key from another Pear instance's Agent Relay Status page. This closes the
current broker session for this project and reconnects it to that workspace.
</p>
<div className="flex flex-wrap items-center gap-2">
<input
type="text"
value={key}
onChange={(event) => {
setKey(event.target.value)
setLocalError(undefined)
}}
placeholder="Workspace key"
disabled={joining}
className="min-w-0 flex-1 rounded-md border border-[var(--pear-border-subtle)] bg-[var(--pear-bg)] px-2 py-1.5 font-mono text-[12px] text-[var(--pear-text)] outline-none focus:border-[var(--pear-accent-dim)]"
/>
<button
type="button"
onClick={submit}
disabled={joining || !key.trim()}
className="inline-flex items-center gap-2 rounded-md border border-[var(--pear-accent-dim)] bg-[var(--pear-accent)]/10 px-3 py-1.5 text-sm text-[var(--pear-accent-bright)] hover:bg-[var(--pear-accent)]/15 disabled:cursor-wait disabled:opacity-60"
>
{joining ? <RefreshCw size={14} className="animate-spin" /> : null}
<span>{joining ? 'Joining' : 'Join workspace'}</span>
</button>
<button
type="button"
onClick={() => {
setOpen(false)
setKey('')
setLocalError(undefined)
}}
disabled={joining}
className="rounded-md border border-[var(--pear-border-subtle)] px-3 py-1.5 text-sm text-[var(--pear-text-dim)] hover:bg-[var(--pear-bg-surface-hover)]"
>
Cancel
</button>
</div>
{localError && <p className="mt-2 text-sm text-[var(--pear-red)]">{localError}</p>}
</div>
)
There was a problem hiding this comment.
Went with the awaited-in-flight-start fix on the main.ts side plus a simpler ref-based auto-close/reset on the UI side (dd0d805) rather than the localError-mirroring approach — avoids a second source of truth for the error string while getting the same close-on-success and clear-on-reopen behavior.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 275b364e0b
ℹ️ 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".
| } | ||
| assertDirectory(cwd, 'Project path') | ||
| await this.shutdown(normalizedProjectId) | ||
| await this.start(normalizedProjectId, cwd, name, win, channels, trimmedKey) |
There was a problem hiding this comment.
Honor the requested key when a start is in flight
When a workspace join races with an existing broker:start for the same project (for example the project auto-start is still spawning, or two join IPCs are submitted close together), this call goes through start(), which sees startPromises and simply waits for/reuses that in-flight session without checking trimmedKey. The join IPC then resolves successfully and notifies integrations even though the broker may still be in the old or first-requested workspace instead of the key the user pasted.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in dd0d805: joinWorkspace now awaits any in-flight startPromises entry for the project before shutting down and pinning the new key, so it can no longer be silently absorbed by a concurrent start().
There was a problem hiding this comment.
5 issues found across 6 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- Guard against joinWorkspace racing an in-flight broker:start for the same project, which could silently ignore the requested workspace key (cubic, codex). - Only notify agents after joinWorkspace actually started a session, matching broker:start's existing guard (cubic). - Auto-close and reset JoinWorkspaceControl on a successful join, and clear stale errors on reopen/cancel (gemini, cubic). - Support Enter-to-submit on the workspace key input (cubic). - Fix ipc-mock's joinWorkspace to match the real 5-arg signature and validate an empty key like the real implementation (cubic). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
--workspace-key/AGENT_RELAY_WORKSPACE_KEYjoin path through a newBrokerManager.joinWorkspace()method,broker:join-workspaceIPC handler, and preload/type bridge, so it's usable at runtime without relaunching the app.Test plan
npm test(123 tests) passesnpx tsc --noEmitclean on both node and web configsnpx eslintclean on touched files