Skip to content

Commit 5808c3d

Browse files
fix(share): follower tabs get an ownership gate instead of a doomed publish; top-right chips stop overlapping
James hit 'Publishing did not finish (Another tab owns this workspace.)' on staging — a follower tab let him configure and publish a share that could only fail, then offered a Resume that re-failed identically. - ShareSheet gains an ownership gate: when another tab holds the writer lease, the create/resume actions are replaced by a plain-language note ('Sharing publishes from the editing tab') with a 'Claim editing here' action wired to the lease acquisition. Verified live with two tabs: the follower's sheet blocks with the note; closing the owning tab self-resolves the gate (desktop auto-edit reacquires) and Create appears without any click. - The breadcrumb's right inset only reserved room for the ReviewBar when a room was ACTIVE, so on a tab with the sheet open pre-room (or a follower) the 'Live · editing with another tab' save chip rendered underneath the anchored Share chip. The inset now accounts for shareOpen; measured 245px clearance in the exact failing state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent de4932d commit 5808c3d

4 files changed

Lines changed: 81 additions & 9 deletions

File tree

web/src/hosted/app/EditorShell.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2192,6 +2192,8 @@
21922192
onInspect={inspectWorkspaceShare}
21932193
onCreate={createWorkspaceShare}
21942194
onStop={stopWorkspaceShare}
2195+
ownershipBlocked={editDenied}
2196+
onClaimOwnership={async () => (await ensureOwnerSession()) !== null}
21952197
onclose={closeShare}
21962198
/>
21972199
{/if}

web/src/hosted/app/HostedDesktopWorkspaceFrame.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@
196196
{actions}
197197
onShare={reviewStore.currentRoomId === null && !shareOpen ? onShare : undefined}
198198
shareEnabled={true}
199-
rightInsetPx={reviewStore.currentRoomId !== null && reviewStore.railMode !== 'expanded'
200-
? 328 - RAIL_WIDTH_PX[reviewStore.railMode]
199+
rightInsetPx={(reviewStore.currentRoomId !== null || shareOpen) && reviewStore.railMode !== 'expanded'
200+
? 328 - (reviewStore.currentRoomId !== null ? RAIL_WIDTH_PX[reviewStore.railMode] : 0)
201201
: 16}
202202
/>
203203
</div>

web/src/hosted/app/ShareSheet.svelte

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
onInspect?: () => Promise<WorkspaceShareView | null>;
3333
onCreate?: (request: WorkspaceShareRequest) => Promise<WorkspaceShareView>;
3434
onStop?: () => Promise<void>;
35+
/** True while another tab holds the workspace writer lease — sharing
36+
* publishes from the editing tab, so creation is gated with a claim
37+
* action instead of failing into a resume loop. */
38+
ownershipBlocked?: boolean;
39+
/** Attempt to claim the writer lease on this tab; true on success. */
40+
onClaimOwnership?: () => Promise<boolean>;
3541
onclose: () => void;
3642
/** Temporary compatibility for callers that have not moved to WorkspaceDetail yet. */
3743
workspaceName?: string;
@@ -46,6 +52,8 @@
4652
onInspect,
4753
onCreate,
4854
onStop,
55+
ownershipBlocked = false,
56+
onClaimOwnership,
4957
onclose,
5058
workspaceName,
5159
}: Props = $props();
@@ -86,6 +94,22 @@
8694
let inviteOptionsOpen = $state(false);
8795
let stopConfirm = $state(false);
8896
let stopBusy = $state(false);
97+
let claimBusy = $state(false);
98+
99+
async function claimOwnership(): Promise<void> {
100+
if (!onClaimOwnership || claimBusy) return;
101+
claimBusy = true;
102+
try {
103+
const granted = await onClaimOwnership();
104+
if (!granted) {
105+
statusMessage = 'The other tab is still editing. Close it (or finish there) and try again.';
106+
} else {
107+
statusMessage = 'This tab can publish now.';
108+
}
109+
} finally {
110+
claimBusy = false;
111+
}
112+
}
89113
let stopSharingButton = $state<HTMLButtonElement | undefined>();
90114
let keepSharingButton = $state<HTMLButtonElement | undefined>();
91115
@@ -427,12 +451,25 @@
427451
{#if operationError}
428452
<p class="share-error" role="alert">{operationError}</p>
429453
{/if}
430-
<div class="share-config-foot">
431-
<p>{configurationHint}</p>
432-
<button class="button primary" type="button" disabled={!configurationReady} onclick={() => void publishShare()}>
433-
Create review link for {manifest.entryCount} {manifest.entryCount === 1 ? 'file' : 'files'}
434-
</button>
435-
</div>
454+
{#if ownershipBlocked}
455+
<div class="share-ownership-note" role="status" data-slot="share-ownership-blocked">
456+
<p>
457+
<strong>Another tab is editing this workspace.</strong>
458+
Sharing publishes from the editing tab — switch to it, or close it
459+
and claim editing here.
460+
</p>
461+
<button class="button" type="button" disabled={claimBusy || !onClaimOwnership} onclick={() => void claimOwnership()}>
462+
{claimBusy ? 'Checking…' : 'Claim editing here'}
463+
</button>
464+
</div>
465+
{:else}
466+
<div class="share-config-foot">
467+
<p>{configurationHint}</p>
468+
<button class="button primary" type="button" disabled={!configurationReady} onclick={() => void publishShare()}>
469+
Create review link for {manifest.entryCount} {manifest.entryCount === 1 ? 'file' : 'files'}
470+
</button>
471+
</div>
472+
{/if}
436473
{:else if phase === 'progress'}
437474
<div class="share-progress share-progress-compact" aria-live="polite">
438475
{#if operationBusy}
@@ -448,11 +485,22 @@
448485
<p class="share-error" role="alert">{operationError}</p>
449486
{/if}
450487
{#if progressPaused}
488+
{#if ownershipBlocked}
489+
<div class="share-ownership-note" role="status" data-slot="share-ownership-blocked">
490+
<p>
491+
<strong>Another tab is editing this workspace.</strong>
492+
Resume from that tab, or claim editing here first.
493+
</p>
494+
<button class="button" type="button" disabled={claimBusy || !onClaimOwnership} onclick={() => void claimOwnership()}>
495+
{claimBusy ? 'Checking…' : 'Claim editing here'}
496+
</button>
497+
</div>
498+
{/if}
451499
<div class="share-foot">
452500
<button class="button" type="button" disabled={!onStop || stopBusy} onclick={() => void stopSharing()}>
453501
{stopBusy ? 'Discarding…' : 'Discard and start over'}
454502
</button>
455-
<button class="button primary" type="button" disabled={!onCreate || operationBusy} onclick={() => void publishShare()}>Resume publishing</button>
503+
<button class="button primary" type="button" disabled={!onCreate || operationBusy || ownershipBlocked} onclick={() => void publishShare()}>Resume publishing</button>
456504
</div>
457505
{/if}
458506
{:else if phase === 'ready' && invite && share}

web/src/hosted/app/app-shell.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,6 +1686,28 @@ a.workspace-row:hover {
16861686
font-size: 0.78rem;
16871687
}
16881688

1689+
/* Ownership gate (share sheet): another tab holds the writer lease, so
1690+
publishing is blocked here — explain and offer the claim action. */
1691+
.share-ownership-note {
1692+
display: flex;
1693+
align-items: center;
1694+
gap: 1rem;
1695+
justify-content: space-between;
1696+
margin-top: 0.75rem;
1697+
padding: 0.7rem 0.9rem;
1698+
border: 1px solid color-mix(in srgb, var(--primary) 35%, transparent);
1699+
border-radius: 8px;
1700+
background: color-mix(in srgb, var(--primary) 6%, transparent);
1701+
}
1702+
.share-ownership-note p {
1703+
margin: 0;
1704+
font: 400 0.85rem/1.45 var(--sans);
1705+
color: var(--foreground);
1706+
}
1707+
.share-ownership-note .button {
1708+
flex-shrink: 0;
1709+
}
1710+
16891711
.share-config-foot {
16901712
display: flex;
16911713
align-items: center;

0 commit comments

Comments
 (0)