a way into the plugin from its detail panel

installing something and then having to guess its url is a small thing that
makes the whole flow feel unfinished. the detail panel now links to the plugin's
screen.

shown only while installed AND enabled, and only when the plugin has a frontend
at all. a link to an unmounted route lands on the home page, because the shell
redirects an unknown path — which reads as a broken link rather than a plugin
that is switched off. a backend-only plugin has no screen to open and gets no
link rather than a dead one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 00:02:09 +00:00
co-authored by Claude Opus 5
parent 543e88a9a6
commit 0e24aa3d52
@@ -1,4 +1,4 @@
import { useSearchParams } from 'react-router'; import { Link, useSearchParams } from 'react-router';
import { usePlugins, type PluginItem } from './usePlugins'; import { usePlugins, type PluginItem } from './usePlugins';
// The right panel: one plugin, and the four verbs. // The right panel: one plugin, and the four verbs.
@@ -105,6 +105,21 @@ export const PluginDetail = () => {
</Row> </Row>
</div> </div>
{/* The way in, when there is one.
Only while installed AND enabled: a link to a route that is not mounted is a link to the home
page, since the shell redirects an unknown path — which reads as the link being broken rather
than the plugin being off. `has.web` because a plugin can be entirely backend and have no screen
to open at all. */}
{plugin.installed && plugin.enabled && plugin.has.web ? (
<Link
to={plugin.prefix}
className="mt-6 inline-flex items-center gap-1.5 text-sm text-duck-teal hover:underline"
>
Open {plugin.label}
<span aria-hidden></span>
</Link>
) : null}
<div className="mt-6 flex flex-wrap gap-2"> <div className="mt-6 flex flex-wrap gap-2">
{!plugin.installed ? ( {!plugin.installed ? (
<Button tone="primary" disabled={busy} onClick={() => run('install', plugin.appName)}> <Button tone="primary" disabled={busy} onClick={() => run('install', plugin.appName)}>