Repository navigation
Join an existing workspace from the broker status page #389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import type React from 'react' | ||
| import { useCallback, useEffect, useMemo, useState } from 'react' | ||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react' | ||
| import { | ||
| Activity, | ||
| AlertCircle, | ||
|
|
@@ -9,6 +9,7 @@ import { | |
| Copy, | ||
| KeyRound, | ||
| List, | ||
| LogOut, | ||
| MessageSquare, | ||
| RefreshCw, | ||
| Server, | ||
|
|
@@ -840,6 +841,108 @@ function BrokerEventFeed({ | |
| ) | ||
| } | ||
|
|
||
| function JoinWorkspaceControl({ | ||
| project, | ||
| joining, | ||
| joinError, | ||
| onJoin, | ||
| onDismissError | ||
| }: { | ||
| project?: Project | ||
| joining?: boolean | ||
| joinError?: string | ||
| onJoin: (project: Project, workspaceKey: string) => void | ||
| onDismissError?: (project: Project) => void | ||
| }): React.ReactNode { | ||
| const [open, setOpen] = useState(false) | ||
| const [key, setKey] = useState('') | ||
| const wasJoining = useRef(false) | ||
|
|
||
| useEffect(() => { | ||
| // Auto-close and reset once a join finishes without leaving an error — | ||
| // otherwise the form stays open with the submitted key and no feedback | ||
| // that the join actually succeeded. | ||
| if (wasJoining.current && !joining && !joinError) { | ||
| setOpen(false) | ||
| setKey('') | ||
| } | ||
| wasJoining.current = Boolean(joining) | ||
| }, [joining, joinError]) | ||
|
|
||
| if (!project) return null | ||
|
|
||
| const submit = (): void => { | ||
| const trimmed = key.trim() | ||
| if (!trimmed) return | ||
| onJoin(project, trimmed) | ||
| } | ||
|
|
||
| const close = (): void => { | ||
| setOpen(false) | ||
| setKey('') | ||
| onDismissError?.(project) | ||
| } | ||
|
|
||
| if (!open) { | ||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={() => { | ||
| onDismissError?.(project) | ||
| 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 ( | ||
| <form | ||
| className="rounded-lg border border-[var(--pear-border-subtle)] bg-[var(--pear-bg-raised)] p-3" | ||
| onSubmit={(event) => { | ||
| event.preventDefault() | ||
| submit() | ||
| }} | ||
| > | ||
| <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} | ||
| autoFocus | ||
| 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="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={close} | ||
| 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>} | ||
| </form> | ||
| ) | ||
|
Comment on lines
+857
to
+943
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. UX Improvement: Auto-close Dialog and Clear Stale ErrorsCurrently, the
We can resolve both issues cleanly by introducing a local
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
|
|
||
| function BrokerCard({ | ||
| broker, | ||
| projectName, | ||
|
|
@@ -848,7 +951,11 @@ function BrokerCard({ | |
| project, | ||
| fixing, | ||
| fixError, | ||
| onAutoFix | ||
| onAutoFix, | ||
| joining, | ||
| joinError, | ||
| onJoinWorkspace, | ||
| onDismissJoinError | ||
| }: { | ||
| broker: BrokerDetails | ||
| projectName: string | ||
|
|
@@ -858,6 +965,10 @@ function BrokerCard({ | |
| fixing?: boolean | ||
| fixError?: string | ||
| onAutoFix?: (project: Project, entry: BrokerErrorEntry) => void | ||
| joining?: boolean | ||
| joinError?: string | ||
| onJoinWorkspace?: (project: Project, workspaceKey: string) => void | ||
| onDismissJoinError?: (project: Project) => void | ||
| }): React.ReactNode { | ||
| const Icon = broker.kind === 'cloud' ? Cloud : Server | ||
|
|
||
|
|
@@ -904,6 +1015,16 @@ function BrokerCard({ | |
| <div className="space-y-5 p-5"> | ||
| <BrokerMetadataSummary broker={broker} /> | ||
|
|
||
| {project && onJoinWorkspace && ( | ||
| <JoinWorkspaceControl | ||
| project={project} | ||
| joining={joining} | ||
| joinError={joinError} | ||
| onJoin={onJoinWorkspace} | ||
| onDismissError={onDismissJoinError} | ||
| /> | ||
| )} | ||
|
|
||
| <div> | ||
| <div className="mb-3 flex items-center gap-2"> | ||
| <KeyRound size={15} className="text-[var(--pear-accent)]" /> | ||
|
|
@@ -975,6 +1096,8 @@ export function BrokerDetailsPage(): React.ReactNode { | |
| const [detailsError, setDetailsError] = useState<string | null>(null) | ||
| const [autoFixingProjectId, setAutoFixingProjectId] = useState<string | null>(null) | ||
| const [autoFixErrors, setAutoFixErrors] = useState<Record<string, string>>({}) | ||
| const [joiningProjectId, setJoiningProjectId] = useState<string | null>(null) | ||
| const [joinErrors, setJoinErrors] = useState<Record<string, string>>({}) | ||
|
|
||
| const pageProjectId = activeBrokerTabProjectId || activeProjectId || undefined | ||
| const pageProjectName = pageProjectId ? getProjectName(projects, pageProjectId) : 'All projects' | ||
|
|
@@ -1087,6 +1210,47 @@ export function BrokerDetailsPage(): React.ReactNode { | |
| } | ||
| }, [loadBrokerDetails]) | ||
|
|
||
| const handleJoinWorkspace = useCallback(async (project: Project, workspaceKey: string): Promise<void> => { | ||
| const root = getDefaultProjectRoot(project) | ||
| if (!root?.pathExists) { | ||
| setJoinErrors((errors) => ({ | ||
| ...errors, | ||
| [project.id]: `Project path no longer exists: ${root?.path || project.rootPath}` | ||
| })) | ||
| return | ||
| } | ||
|
|
||
| setJoiningProjectId(project.id) | ||
| setJoinErrors((errors) => { | ||
| const { [project.id]: _removed, ...remaining } = errors | ||
| return remaining | ||
| }) | ||
| try { | ||
| await pear.broker.joinWorkspace( | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| project.id, | ||
| root.path, | ||
| getProjectBrokerName(project), | ||
| project.channels, | ||
| workspaceKey | ||
| ) | ||
| await loadBrokerDetails() | ||
| } catch (err) { | ||
| setJoinErrors((errors) => ({ | ||
| ...errors, | ||
| [project.id]: err instanceof Error ? err.message : String(err) | ||
| })) | ||
| } finally { | ||
| setJoiningProjectId(null) | ||
| } | ||
| }, [loadBrokerDetails]) | ||
|
|
||
| const handleDismissJoinError = useCallback((project: Project): void => { | ||
| setJoinErrors((errors) => { | ||
| const { [project.id]: _removed, ...remaining } = errors | ||
| return remaining | ||
| }) | ||
| }, []) | ||
|
|
||
| useEffect(() => { | ||
| void loadBrokerDetails() | ||
| }, [loadBrokerDetails]) | ||
|
|
@@ -1145,6 +1309,10 @@ export function BrokerDetailsPage(): React.ReactNode { | |
| fixing={autoFixingProjectId === broker.projectId} | ||
| fixError={autoFixErrors[broker.projectId]} | ||
| onAutoFix={(project, entry) => void handleAutoFix(project, entry)} | ||
| joining={joiningProjectId === broker.projectId} | ||
| joinError={joinErrors[broker.projectId]} | ||
| onJoinWorkspace={(project, workspaceKey) => void handleJoinWorkspace(project, workspaceKey)} | ||
| onDismissJoinError={handleDismissJoinError} | ||
| /> | ||
| ))} | ||
| {unmatchedProjectErrorGroups.map(([projectId, entries]) => ( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.