every plugin route renders a workspace, and it is not a rule you can forget
an exclusionary rule, made structural. a plugin does not render a screen: it
contributes panels and says how they are arranged, and the shell renders
WorkspaceView around them.
web/panels.ts appRegistryMetas — at least one panel
web/layout.ts defaultLayout — how they are arranged
both required the moment web/ exists, and missing either is refused at discovery
by name and with the reason. tested:
probeplug: has a web/ directory but is missing web/layout.ts.
Every plugin route renders a Workspace: contribute panels and a layout,
not a screen.
there is deliberately no way to export a component. one that could would be free
to render a bare div, a full-page form, or its own navigation, and the platform
would become a shell hosting strangers' layouts rather than one application.
non-compliance is not so much refused as unrepresentable — there is nowhere to
put a screen.
the shell registers <prefix> and <prefix>/:section, exactly as the core screens
do, so a plugin's sections stay addressable and cmd-clickable, and panels read
useParams independently rather than passing state between themselves.
appTypes.allowed is pinned to that plugin's own keys, so a persisted layout
naming something else falls back instead of rendering another plugin's panel
inside this screen.
the example plugin is rebuilt to model it — two panels, a layout, one of them
calling its own /api/example/ping through useClient — because the reference
implementation is what everyone copies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+14
-14
@@ -69,7 +69,9 @@ export const BrowserRelay = () => {
|
||||
<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>
|
||||
<a href="/browser" className="text-duck-teal underline">
|
||||
view tabs
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,7 +98,9 @@ export const BrowserRelay = () => {
|
||||
<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>{' '}
|
||||
<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>
|
||||
@@ -139,12 +143,7 @@ export const BrowserRelay = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => regenerate.mutate()}
|
||||
disabled={regenerate.isPending}
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={() => regenerate.mutate()} disabled={regenerate.isPending}>
|
||||
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
|
||||
Regenerate
|
||||
</Button>
|
||||
@@ -166,8 +165,8 @@ export const BrowserRelay = () => {
|
||||
<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{' '}
|
||||
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>{' '}
|
||||
@@ -189,10 +188,11 @@ type CredentialRowProps = {
|
||||
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">
|
||||
<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-green-500" /> : <Copy className="h-3.5 w-3.5 opacity-50" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user