Skip to content

Join an existing workspace from the broker status page - #389

Merged
willwashburn merged 2 commits into
mainfrom
feat/join-existing-workspace
Jul 1, 2026
Merged

willwashburn merged 2 commits into
mainfrom
feat/join-existing-workspace

Conversation

@willwashburn

@willwashburn willwashburn commented Jul 1, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Adds a "Close this workspace and join new one" control to each project's card on the Agent Relay Status page, so a project's broker can join a workspace key copied from another Pear instance instead of always creating its own.
  • Wires the existing (previously env-var-only) --workspace-key / AGENT_RELAY_WORKSPACE_KEY join path through a new BrokerManager.joinWorkspace() method, broker:join-workspace IPC handler, and preload/type bridge, so it's usable at runtime without relaunching the app.

Test plan

  • npm test (123 tests) passes
  • npx tsc --noEmit clean on both node and web configs
  • npx eslint clean on touched files
  • Manual: paste a workspace key from one Pear instance's status page into another and confirm the second instance's agents/messages appear in the shared workspace

Review in cubic

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

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

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

Next review available in: 29 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 40da6b77-d756-4029-b7f7-859a07d38586

📥 Commits

Reviewing files that changed from the base of the PR and between 5b656e0 and dd0d805.

📒 Files selected for processing (6)
  • src/main/broker.ts
  • src/main/ipc-handlers.ts
  • src/preload/index.ts
  • src/renderer/src/components/broker/BrokerDetailsPage.tsx
  • src/renderer/src/lib/ipc-mock.ts
  • src/shared/types/ipc.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/join-existing-workspace

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.

@gemini-code-assist gemini-code-assist 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.

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.

Comment on lines +855 to +917
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&apos;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>
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

UX Improvement: Auto-close Dialog and Clear Stale Errors

Currently, the JoinWorkspaceControl has two usability issues:

  1. 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.
  2. 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&apos;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>
  )

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

@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: 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".

Comment thread src/main/broker.ts Outdated
}
assertDirectory(cwd, 'Project path')
await this.shutdown(normalizedProjectId)
await this.start(normalizedProjectId, cwd, name, win, channels, trimmedKey)

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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().

@cubic-dev-ai cubic-dev-ai 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.

5 issues found across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/renderer/src/components/broker/BrokerDetailsPage.tsx
Comment thread src/renderer/src/lib/ipc-mock.ts Outdated
Comment thread src/main/ipc-handlers.ts Outdated
Comment thread src/main/broker.ts Outdated
Comment thread src/renderer/src/components/broker/BrokerDetailsPage.tsx
- 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>
@willwashburn
willwashburn merged commit 9bd5cea into main Jul 1, 2026
5 checks passed
@willwashburn
willwashburn deleted the feat/join-existing-workspace branch July 1, 2026 04:34
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