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
12 changes: 12 additions & 0 deletions .changeset/dashboard-docs-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"browserhive": patch
---

Dashboard: links to the documentation on browserhive.ai, the website and GitHub.

- A Help menu in the top bar opens the guide for the current page, the dashboard guide, MCP client setup, troubleshooting, release notes and issue reporting.
- The sidebar footer links to Docs, GitHub and the website, and the version links to its release notes.
- The command palette can open the docs, the tool reference, the website and GitHub.
- Info popovers are fixed: inline code and bold text no longer break onto their own lines. They now have paragraph spacing and a "Read the docs" link.
- New explainers were added across Overview, Websites, Vault, Vault log, System (tokens, configuration, storage, migrations, degradations) and session details.
- System setting hints now use the real camelCase flags (`--maxSessions`, `--allowEvaluate`, `--retentionDays`) and link to each key in the configuration reference.
139 changes: 139 additions & 0 deletions packages/dashboard/src/app/shell/HelpMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/** @module app/shell/HelpMenu — topbar help dropdown: the docs section for the current page, guides, keyboard shortcuts, and the project links (website, GitHub, release notes, issues); every outbound item opens in a new tab */
import { useRouterState } from '@tanstack/react-router';
import type { ReactNode } from 'react';
import { Button } from '@/components/ui/button.tsx';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu.tsx';
import { Hint } from '@/components/ui/tooltip.tsx';
import { ICONS, type IconName } from '@/lib/icons.ts';
import {
type DocsPage,
docsUrl,
ISSUES_URL,
RELEASES_URL,
REPO_URL,
releaseUrl,
WEBSITE_URL,
} from '@/lib/links.ts';
import { useNavSignals } from './SidebarNav.tsx';

/** Dashboard guide section for a route (`/vault/log` → `dashboardVaultLog`). */
export function pageDocs(pathname: string): { readonly key: DocsPage; readonly label: string } {
const first = pathname.split('/').filter(Boolean);
const [top, second] = first;
switch (top) {
case 'sessions':
return second === undefined
? { key: 'dashboardSessions', label: 'Sessions' }
: { key: 'dashboardSessionDetail', label: 'Session detail' };
case 'attention':
return { key: 'dashboardAttention', label: 'Attention' };
case 'websites':
return { key: 'dashboardWebsites', label: 'Websites' };
case 'blocklist':
return { key: 'dashboardBlocklist', label: 'Blocklist' };
case 'vault':
return second === 'log'
? { key: 'dashboardVaultLog', label: 'Vault log' }
: { key: 'dashboardVault', label: 'Vault' };
case 'logs':
return { key: 'dashboardLogs', label: 'Logs' };
case 'system':
return { key: 'dashboardSystem', label: 'System' };
case 'notifications':
return { key: 'dashboardNotifications', label: 'Notifications' };
default:
return { key: 'dashboardOverview', label: 'Overview' };
}
}

function ExternalItem({
href,
icon,
children,
}: {
readonly href: string;
readonly icon: IconName;
readonly children: ReactNode;
}) {
const Icon = ICONS[icon];
const External = ICONS.external;
return (
<DropdownMenuItem render={<a href={href} target="_blank" rel="noreferrer" />}>
<Icon aria-hidden="true" />
<span className="flex-1">{children}</span>
<External aria-hidden="true" className="ml-auto size-3.5 text-subtle-foreground" />
</DropdownMenuItem>
);
}

/** Help menu. */
export function HelpMenu({ onOpenKeyboardMap }: { readonly onOpenKeyboardMap: () => void }) {
const pathname = useRouterState({ select: (s) => s.location.pathname });
const { version } = useNavSignals();
const here = pageDocs(pathname);
const Help = ICONS.help;
const Keyboard = ICONS.keyboard;
return (
<DropdownMenu>
<Hint label="Help and docs">
<DropdownMenuTrigger
render={<Button type="button" variant="ghost" size="icon" aria-label="Help and docs" />}
>
<Help aria-hidden="true" />
</DropdownMenuTrigger>
</Hint>
<DropdownMenuContent align="end" className="w-64">
<DropdownMenuGroup>
<DropdownMenuLabel>Documentation</DropdownMenuLabel>
<ExternalItem href={docsUrl(here.key)} icon="book">
Help for {here.label}
</ExternalItem>
<ExternalItem href={docsUrl('dashboard')} icon="overview">
Dashboard guide
</ExternalItem>
<ExternalItem href={docsUrl('mcpClients')} icon="link">
Connect an MCP client
</ExternalItem>
<ExternalItem href={docsUrl('troubleshooting')} icon="tool">
Troubleshooting
</ExternalItem>
<ExternalItem href={docsUrl('home')} icon="layers">
All documentation
</ExternalItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={onOpenKeyboardMap}>
<Keyboard aria-hidden="true" /> Keyboard shortcuts
<DropdownMenuShortcut>?</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>BrowserHive</DropdownMenuLabel>
<ExternalItem href={WEBSITE_URL} icon="globe">
browserhive.ai
</ExternalItem>
<ExternalItem href={REPO_URL} icon="github">
GitHub repository
</ExternalItem>
<ExternalItem href={version !== null ? releaseUrl(version) : RELEASES_URL} icon="rocket">
{version !== null ? `What's new in v${version}` : 'Release notes'}
</ExternalItem>
<ExternalItem href={ISSUES_URL} icon="bug">
Report an issue
</ExternalItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
106 changes: 88 additions & 18 deletions packages/dashboard/src/app/shell/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { useRouterState } from '@tanstack/react-router';
import { useEffect } from 'react';
import { useShortcut } from '@/app/providers/KeyboardProvider.tsx';
import { BrandMark } from '@/components/shared/brand-mark.tsx';
import { Button } from '@/components/ui/button.tsx';
import { Button, buttonVariants } from '@/components/ui/button.tsx';
import { Kbd } from '@/components/ui/kbd.tsx';
import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet.tsx';
import { Hint } from '@/components/ui/tooltip.tsx';
import { ICONS } from '@/lib/icons.ts';
import { formatCombo } from '@/lib/keyboard.ts';
import { DOCS_URL, RELEASES_URL, REPO_URL, releaseUrl, WEBSITE_URL } from '@/lib/links.ts';
import { cn } from '@/lib/utils.ts';
import { isMacLike } from './platform.ts';
import { SidebarNav, useNavSignals } from './SidebarNav.tsx';
Expand Down Expand Up @@ -88,9 +89,33 @@ function Footer({
}) {
const { version } = useNavSignals();
const Keyboard = ICONS.keyboard;
const Book = ICONS.book;
const GitHub = ICONS.github;
if (collapsed) {
return (
<div className="flex shrink-0 flex-col items-center gap-1 border-t border-sidebar-border py-3 pointer-coarse:hidden">
<div className="flex shrink-0 flex-col items-center gap-1 border-t border-sidebar-border py-3">
<Hint side="right" label="Documentation">
<a
href={DOCS_URL}
target="_blank"
rel="noreferrer"
aria-label="Documentation (opens browserhive.ai)"
className={buttonVariants({ variant: 'ghost', size: 'icon' })}
>
<Book aria-hidden="true" className="size-5" />
</a>
</Hint>
<Hint side="right" label="GitHub repository">
<a
href={REPO_URL}
target="_blank"
rel="noreferrer"
aria-label="GitHub repository"
className={buttonVariants({ variant: 'ghost', size: 'icon' })}
>
<GitHub aria-hidden="true" className="size-5" />
</a>
</Hint>
<Hint side="right" label="Keyboard shortcuts" shortcut="?">
<Button
type="button"
Expand All @@ -106,26 +131,71 @@ function Footer({
);
}
return (
<div className="flex shrink-0 items-center justify-between gap-2 border-t border-sidebar-border px-3 py-3">
<Button
type="button"
variant="ghost"
size="sm"
// Keyboard shortcuts are noise on touch screens.
className="text-muted-foreground pointer-coarse:hidden"
onClick={onOpenKeyboardMap}
>
<Keyboard aria-hidden="true" />
Shortcuts
<Kbd className="ml-1">?</Kbd>
</Button>
{version !== null ? (
<span className="ml-auto px-1 font-mono text-xs text-muted-foreground">v{version}</span>
) : null}
<div className="flex shrink-0 flex-col gap-1 border-t border-sidebar-border px-3 pt-2 pb-3">
<nav aria-label="Resources" className="grid grid-cols-3 gap-1">
<FooterLink href={DOCS_URL} icon={<Book aria-hidden="true" />}>
Docs
</FooterLink>
<FooterLink href={REPO_URL} icon={<GitHub aria-hidden="true" />}>
GitHub
</FooterLink>
<FooterLink href={WEBSITE_URL} icon={<ICONS.globe aria-hidden="true" />}>
Website
</FooterLink>
</nav>
<div className="flex items-center justify-between gap-2">
<Button
type="button"
variant="ghost"
size="sm"
// Keyboard shortcuts are noise on touch screens.
className="text-muted-foreground pointer-coarse:hidden"
onClick={onOpenKeyboardMap}
>
<Keyboard aria-hidden="true" />
Shortcuts
<Kbd className="ml-1">?</Kbd>
</Button>
{version !== null ? (
<Hint side="top" label={`Release notes for v${version}`}>
<a
href={version.length > 0 ? releaseUrl(version) : RELEASES_URL}
target="_blank"
rel="noreferrer"
className="ml-auto rounded-sm px-1 font-mono text-xs text-muted-foreground underline-offset-4 transition-colors focus-ring hover:text-foreground hover:underline"
>
v{version}
</a>
</Hint>
) : null}
</div>
</div>
);
}

/** A small outbound link in the sidebar footer (opens in a new tab). */
function FooterLink({
href,
icon,
children,
}: {
readonly href: string;
readonly icon: React.ReactNode;
readonly children: React.ReactNode;
}) {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
className="flex h-8 items-center justify-center gap-1.5 rounded-md px-1 text-xs font-medium whitespace-nowrap text-muted-foreground transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-ring [&_svg]:size-3.5 [&_svg]:shrink-0"
>
{icon}
{children}
</a>
);
}

/** Sidebar. */
export function Sidebar({ band, drawerOpen, onDrawerOpenChange, onOpenKeyboardMap }: SidebarProps) {
const [preference, setPreference] = useSidebarPreference();
Expand Down
2 changes: 2 additions & 0 deletions packages/dashboard/src/app/shell/Topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Hint } from '@/components/ui/tooltip.tsx';
import { ICONS } from '@/lib/icons.ts';
import { formatCombo } from '@/lib/keyboard.ts';
import { HealthPill } from './HealthPill.tsx';
import { HelpMenu } from './HelpMenu.tsx';
import { NotificationBell } from './NotificationBell.tsx';
import { PrincipalMenu } from './PrincipalMenu.tsx';
import { isMacLike } from './platform.ts';
Expand Down Expand Up @@ -121,6 +122,7 @@ export function Topbar({
)}
<HealthPill compact={band === 'sm' || band === 'md'} />
<NotificationBell />
<HelpMenu onOpenKeyboardMap={onOpenKeyboardMap} />
<ThemeMenu />
<PrincipalMenu
compact={band !== 'wide' && band !== 'lg'}
Expand Down
70 changes: 70 additions & 0 deletions packages/dashboard/src/app/shell/use-palette-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCallback, useMemo } from 'react';
import { useApi, useAuth } from '@/app/providers/AuthProvider.tsx';
import { keys } from '@/lib/api/keys.ts';
import type { IconName } from '@/lib/icons.ts';
import { docsUrl, ISSUES_URL, REPO_URL, WEBSITE_URL } from '@/lib/links.ts';
import { SESSION_STATE, type StatusEntry, sessionDisplayState } from '@/lib/status-registry.ts';
import { readStorageJson, writeStorageJson } from '@/lib/storage.ts';
import { useTheme } from '@/theme/ThemeProvider.tsx';
Expand Down Expand Up @@ -49,6 +50,74 @@ export const PALETTE_GROUPS: readonly PaletteGroup[] = [
const RECENT_KEY = 'bh.palette.recent';
const RECENT_LIMIT = 5;

/** Opens an outbound link in a new tab. */
function openExternal(href: string): () => void {
return () => {
window.open(href, '_blank', 'noopener,noreferrer');
};
}

/** Outbound links to the docs and the project. */
const DOCS_COMMANDS: readonly PaletteCommand[] = [
{
id: 'action:docs',
group: 'Actions',
label: 'Open documentation',
hint: 'browserhive.ai/docs',
icon: 'book',
keywords: ['docs', 'help', 'guide', 'manual'],
run: openExternal(docsUrl('home')),
},
{
id: 'action:docs:dashboard',
group: 'Actions',
label: 'Open the dashboard guide',
icon: 'book',
keywords: ['docs', 'help', 'dashboard'],
run: openExternal(docsUrl('dashboard')),
},
{
id: 'action:docs:mcp-clients',
group: 'Actions',
label: 'How to connect an MCP client',
icon: 'book',
keywords: ['docs', 'claude', 'cursor', 'vs code', 'setup', 'connect'],
run: openExternal(docsUrl('mcpClients')),
},
{
id: 'action:docs:tools',
group: 'Actions',
label: 'Open the MCP tool reference',
icon: 'book',
keywords: ['docs', 'tools', 'reference', 'api'],
run: openExternal(docsUrl('tools')),
},
{
id: 'action:website',
group: 'Actions',
label: 'Open browserhive.ai',
icon: 'globe',
keywords: ['website', 'home'],
run: openExternal(WEBSITE_URL),
},
{
id: 'action:github',
group: 'Actions',
label: 'Open the GitHub repository',
icon: 'github',
keywords: ['github', 'source', 'code', 'repo'],
run: openExternal(REPO_URL),
},
{
id: 'action:issue',
group: 'Actions',
label: 'Report an issue',
icon: 'bug',
keywords: ['bug', 'github', 'feedback'],
run: openExternal(ISSUES_URL),
},
];

/** Substring match over label, hint and keywords. */
export function matchesQuery(
command: Pick<PaletteCommand, 'label' | 'hint' | 'keywords'>,
Expand Down Expand Up @@ -213,6 +282,7 @@ export function usePaletteCommands(
keywords: ['account', 'security'],
run: () => void navigate({ to: '/change-password', search: { voluntary: true } }),
},
...DOCS_COMMANDS,
{
id: 'action:logout',
group: 'Actions',
Expand Down
Loading
Loading