browser pairing and its extension zip leave the platform
The last two pieces the extraction missed. IntegrationsSettings/BrowserRelay.tsx drove the plugin's pairing from a platform screen, calling /browser/relay-token and working only because the plugin happens to serve that path — the same coupling removed everywhere else this morning. public/browser-relay-extension.zip was a hand-built snapshot that had drifted 58 lines behind the extension it claimed to be: built in August from something predating a February commit, never rebuilt, and missing the multi-session recovery path. The plugin builds the archive from its own directory per request now, so there is nothing left to fall behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
-199
@@ -1,199 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Copy, Check, Download, ExternalLink, RefreshCw, Trash2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { copyToClipboard } from 'helpers/clipboard';
|
||||
|
||||
type RelayToken = {
|
||||
token: string;
|
||||
port: number;
|
||||
};
|
||||
|
||||
export const BrowserRelay = () => {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const [copiedField, setCopiedField] = useState<string | null>(null);
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: ['browser-status'],
|
||||
queryFn: () => client.get<{ extensionConnected: boolean; targetCount: number }>('/browser/status'),
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const { data: tokenData } = useQuery({
|
||||
queryKey: ['browser-relay-token'],
|
||||
queryFn: () => client.get<RelayToken>('/browser/relay-token'),
|
||||
});
|
||||
|
||||
const regenerate = useMutation({
|
||||
mutationFn: () => client.post<RelayToken>('/browser/relay-token/regenerate'),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['browser-relay-token'] });
|
||||
toast.success('Token regenerated — update the extension with the new token');
|
||||
},
|
||||
onError: () => toast.error('Failed to regenerate token'),
|
||||
});
|
||||
|
||||
const revoke = useMutation({
|
||||
mutationFn: () => client.delete('/browser/relay-token'),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['browser-relay-token'] });
|
||||
toast.success('Token revoked');
|
||||
},
|
||||
onError: () => toast.error('Failed to revoke token'),
|
||||
});
|
||||
|
||||
const handleCopy = async (value: string, field: string) => {
|
||||
try {
|
||||
await copyToClipboard(value);
|
||||
setCopiedField(field);
|
||||
toast.success('Copied to clipboard');
|
||||
setTimeout(() => setCopiedField(null), 2000);
|
||||
} catch {
|
||||
toast.error('Failed to copy');
|
||||
}
|
||||
};
|
||||
|
||||
const isConnected = status?.extensionConnected ?? false;
|
||||
const serverAddress = window.location.hostname;
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
{isConnected ? (
|
||||
<div className="flex items-center gap-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-success shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-duck-dark dark:text-foreground">Connected</p>
|
||||
<p className="text-xs text-duck-dark/50 dark:text-foreground/50">
|
||||
{status?.targetCount ?? 0} tab{(status?.targetCount ?? 0) !== 1 ? 's' : ''} attached
|
||||
{' — '}
|
||||
<a href="/browser" className="text-duck-teal underline">
|
||||
view tabs
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-duck-dark/60 dark:text-foreground/60">
|
||||
Connect your Chrome browser to Officer so AI agents can view and interact with your tabs.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Step 1: Install */}
|
||||
<div className="rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4 grid gap-3">
|
||||
<p className="text-sm font-medium text-duck-dark dark:text-foreground">1. Install the extension</p>
|
||||
<p className="text-xs text-duck-dark/50 dark:text-foreground/50">
|
||||
Download and unzip the extension, then load it in Chrome:
|
||||
</p>
|
||||
<a
|
||||
href="/browser-relay-extension.zip"
|
||||
download
|
||||
className="flex items-center justify-center gap-2 w-full h-11 rounded-md border border-duck-dark/15 dark:border-foreground/15 text-sm font-medium hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download Extension
|
||||
</a>
|
||||
<ol className="list-decimal list-inside space-y-1.5 text-xs text-duck-dark/50 dark:text-foreground/50">
|
||||
<li>
|
||||
Open{' '}
|
||||
<code className="bg-duck-dark/5 dark:bg-foreground/5 px-1.5 py-0.5 rounded text-[11px]">
|
||||
chrome://extensions
|
||||
</code>{' '}
|
||||
in Chrome
|
||||
</li>
|
||||
<li>
|
||||
Enable <strong>Developer mode</strong> (top-right toggle)
|
||||
</li>
|
||||
<li>
|
||||
Click <strong>Load unpacked</strong> and select the unzipped folder
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{/* Step 2: Configure */}
|
||||
{tokenData && (
|
||||
<div className="rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4 grid gap-3">
|
||||
<p className="text-sm font-medium text-duck-dark dark:text-foreground">2. Configure the extension</p>
|
||||
<p className="text-xs text-duck-dark/50 dark:text-foreground/50">
|
||||
Open the extension options (right-click icon → Options) and enter these values:
|
||||
</p>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<CredentialRow
|
||||
label="Server address"
|
||||
value={serverAddress}
|
||||
copied={copiedField === 'host'}
|
||||
onCopy={() => handleCopy(serverAddress, 'host')}
|
||||
/>
|
||||
<CredentialRow
|
||||
label="Port"
|
||||
value={String(tokenData.port)}
|
||||
copied={copiedField === 'port'}
|
||||
onCopy={() => handleCopy(String(tokenData.port), 'port')}
|
||||
/>
|
||||
<CredentialRow
|
||||
label="Relay token"
|
||||
value={tokenData.token}
|
||||
masked
|
||||
copied={copiedField === 'token'}
|
||||
onCopy={() => handleCopy(tokenData.token, 'token')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => regenerate.mutate()} disabled={regenerate.isPending}>
|
||||
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
|
||||
Regenerate
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => revoke.mutate()}
|
||||
disabled={revoke.isPending}
|
||||
className="text-red-500 hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
|
||||
Revoke
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Attach */}
|
||||
<div className="rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4 grid gap-3">
|
||||
<p className="text-sm font-medium text-duck-dark dark:text-foreground">3. Attach a tab</p>
|
||||
<p className="text-xs text-duck-dark/50 dark:text-foreground/50">
|
||||
Navigate to any webpage and click the Officer extension icon in the toolbar. A cyan <strong>ON</strong> badge
|
||||
means the tab is connected. Then go to{' '}
|
||||
<a href="/browser" className="text-duck-teal underline inline-flex items-center gap-0.5">
|
||||
/browser <ExternalLink className="h-3 w-3" />
|
||||
</a>{' '}
|
||||
to view your tabs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type CredentialRowProps = {
|
||||
label: string;
|
||||
value: string;
|
||||
masked?: boolean;
|
||||
copied: boolean;
|
||||
onCopy: () => void;
|
||||
};
|
||||
|
||||
const CredentialRow = ({ label, value, masked, copied, onCopy }: CredentialRowProps) => (
|
||||
<div className="flex items-center gap-2 rounded-md bg-duck-dark/5 dark:bg-foreground/5 px-3 py-2">
|
||||
<span className="text-xs text-duck-dark/50 dark:text-foreground/50 shrink-0 w-24">{label}</span>
|
||||
<code className="flex-1 text-xs truncate">{masked ? `${value.slice(0, 8)}${'•'.repeat(16)}` : value}</code>
|
||||
<button
|
||||
onClick={onCopy}
|
||||
className="shrink-0 p-1 rounded hover:bg-duck-dark/10 dark:hover:bg-foreground/10 cursor-pointer"
|
||||
>
|
||||
{copied ? <Check className="h-3.5 w-3.5 text-success" /> : <Copy className="h-3.5 w-3.5 opacity-50" />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -8,7 +8,6 @@ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { SettingsRoute, SettingsSidebar, SettingsContent, type SettingsSection } from '../SettingsPanel';
|
||||
import { GoogleOAuthConfig } from './GoogleOAuthConfig';
|
||||
// import { GoogleAccount } from './GoogleAccount'; // hidden — email uses IMAP app password now
|
||||
import { BrowserRelay } from './BrowserRelay';
|
||||
import { ApifyConfig } from './ApifyConfig';
|
||||
import { DavAppPasswords } from './DavAppPasswords';
|
||||
|
||||
@@ -50,13 +49,6 @@ const personalSections: SettingsSection[] = [
|
||||
description: 'Per-device passwords for CalDAV/CardDAV clients',
|
||||
content: <DavAppPasswords />,
|
||||
},
|
||||
{
|
||||
key: 'browser-relay',
|
||||
icon: Globe,
|
||||
title: 'Browser Relay',
|
||||
description: 'Connect your Chrome browser',
|
||||
content: <BrowserRelay />,
|
||||
},
|
||||
];
|
||||
|
||||
// The Enterprise/Personal tab is not a second thing to remember — it is a fact about which section is
|
||||
|
||||
Reference in New Issue
Block a user