Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hide-only-you-session-share-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@truefoundry/trueforge-ui': patch
---

Hide the session share URL when access is Only you.
60 changes: 32 additions & 28 deletions packages/trueforge-ui/src/atoms/ShareSessionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,34 +129,38 @@ export function ShareSessionDialog({ sessionId, trigger }: ShareSessionDialogPro
void changePermission(next);
}}
/>
<div role="separator" className="bg-border h-px w-full" />
<div className="flex w-full items-center gap-2">
<Icon name="link" size="1rem" className="text-text-primary shrink-0" />
<p className="text-text-primary min-w-0 flex-1 text-sm font-semibold">Share URL</p>
</div>
<div className="flex h-8 w-full items-center gap-2">
<input
readOnly
value={shareUrl}
aria-label="Share URL"
className={auiInputClass('h-8 min-w-0 flex-1 px-3.5 text-[0.8125rem]')}
/>
<Button.Primary
type="button"
size="large"
className="h-8 shrink-0 rounded px-2.5 text-xs"
onClick={() => {
track(AnalyticsEvents.Session.SHARE_LINK_COPIED, {
session_id: sessionId,
permission,
});
copySharedSessionLink();
}}
>
<Icon name={copied ? 'check' : 'copy'} size="0.875rem" />
{copied ? 'Copied' : 'Copy'}
</Button.Primary>
</div>
{permission === 'tenant' ? (
<>
<div role="separator" className="bg-border h-px w-full" />
<div className="flex w-full items-center gap-2">
<Icon name="link" size="1rem" className="text-text-primary shrink-0" />
<p className="text-text-primary min-w-0 flex-1 text-sm font-semibold">Share URL</p>
</div>
<div className="flex h-8 w-full items-center gap-2">
<input
readOnly
value={shareUrl}
aria-label="Share URL"
className={auiInputClass('h-8 min-w-0 flex-1 px-3.5 text-[0.8125rem]')}
/>
<Button.Primary
type="button"
size="large"
className="h-8 shrink-0 rounded px-2.5 text-xs"
onClick={() => {
track(AnalyticsEvents.Session.SHARE_LINK_COPIED, {
session_id: sessionId,
permission,
});
copySharedSessionLink();
}}
>
<Icon name={copied ? 'check' : 'copy'} size="0.875rem" />
{copied ? 'Copied' : 'Copy'}
</Button.Primary>
</div>
</>
) : null}
</DropdownMenu>
);
}
Expand Down
13 changes: 11 additions & 2 deletions packages/trueforge-ui/test/atoms/SessionsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,21 @@ describe('SessionsPage', () => {

it('copies the no-router shared-session query URL for the selected session', async () => {
window.history.replaceState(null, '', '/?view=sessions&sessionId=sess-1&agentId=agent-1&s_tw=30');
renderPage();
renderPage({
getSession: vi.fn(async (): Promise<Session> => ({
id: 'sess-1',
title: 'Named session',
isMutable: false,
shared: true,
createdAt: namedRow.createdAt,
updatedAt: namedRow.updatedAt,
})),
});

fireEvent.click(await screen.findByRole('button', { name: 'Share' }));
expect(await screen.findByText('Change permissions')).toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Copy' }));
fireEvent.click(await screen.findByRole('button', { name: 'Copy' }));

await waitFor(() => {
expect(clipboardWriteText).toHaveBeenCalledOnce();
Expand Down
19 changes: 15 additions & 4 deletions packages/trueforge-ui/test/atoms/ShareChatButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,30 @@ describe('ShareChatButton', () => {

it('opens the share popover and copies the routed shared-session URL', async () => {
activeThread.remoteId = 'session-1';
const server = createMockAgentUIServer({
getSession: vi.fn(async () => ({
id: 'session-1',
isMutable: true,
shared: true,
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
})),
});
render(
<SlotsProvider>
<ResolvedRoutesProvider routes={resolveRoutesConfig()}>
<ShareChatButton />
</ResolvedRoutesProvider>
<ServerProvider server={server}>
<ResolvedRoutesProvider routes={resolveRoutesConfig()}>
<ShareChatButton />
</ResolvedRoutesProvider>
</ServerProvider>
</SlotsProvider>,
);

fireEvent.click(screen.getByRole('button', { name: 'Share' }));
expect(await screen.findByText('Change permissions')).toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: 'Copy' }));
fireEvent.click(await screen.findByRole('button', { name: 'Copy' }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledOnce();
});
Expand Down
42 changes: 41 additions & 1 deletion packages/trueforge-ui/test/atoms/ShareSessionDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,28 @@ async function openSharePopover() {
}

describe('ShareSessionDialog', () => {
it('loads current sharing and copies the share URL', async () => {
it('hides the share URL while access is Only you', async () => {
renderDialog();
await openSharePopover();

expect(await screen.findByRole('button', { name: 'Session sharing' })).toHaveTextContent('Only you');
expect(screen.queryByLabelText('Share URL')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Copy' })).not.toBeInTheDocument();
});

it('loads tenant sharing and copies the share URL', async () => {
renderDialog({
getSession: vi.fn(async () => ({
id: 'session-1',
isMutable: true,
shared: true,
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
})),
});
await openSharePopover();

expect(await screen.findByRole('button', { name: 'Session sharing' })).toHaveTextContent('Everyone within acme');
expect((screen.getByLabelText('Share URL') as HTMLInputElement).value).toContain('/sessions/share/session-1');

fireEvent.click(screen.getByRole('button', { name: 'Copy' }));
Expand All @@ -100,6 +117,29 @@ describe('ShareSessionDialog', () => {
expect(screen.getByRole('button', { name: 'Copied' })).toBeInTheDocument();
});

it('shows the share URL after switching to tenant access and hides it again for Only you', async () => {
const { updateSession } = renderDialog();
await openSharePopover();

expect(screen.queryByLabelText('Share URL')).not.toBeInTheDocument();

fireEvent.click(await screen.findByRole('button', { name: 'Session sharing' }));
fireEvent.click(await screen.findByRole('option', { name: 'Everyone within acme' }));

await waitFor(() => {
expect(updateSession).toHaveBeenCalledWith({ sessionId: 'session-1', shared: true });
});
expect(screen.getByLabelText('Share URL')).toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: 'Session sharing' }));
fireEvent.click(await screen.findByRole('option', { name: 'Only you' }));

await waitFor(() => {
expect(updateSession).toHaveBeenCalledWith({ sessionId: 'session-1', shared: false });
});
expect(screen.queryByLabelText('Share URL')).not.toBeInTheDocument();
});

it('closes permission options on outside click without PATCHing', async () => {
const { updateSession } = renderDialog();
await openSharePopover();
Expand Down
Loading