feat(desktop): multi-transport hosts with relay fallback, card-style services dropdown
- A saved host now keeps every transport its pairing link carried: direct URL plus the relay descriptor, with one token for both (the mobile connection model). Switching tries the direct leg and falls back to the E2EE tunnel; list probes report Connected · Relay when only the tunnel reaches the host; relaunch restore picks direct first - Host switching trusts the dropdown's fresh probe instead of re-probing on click (no doubled latency, no transient Unreachable flashes); statuses are written once with the final outcome, survive the dropdown closing via a last-known cache, and an unprobed host reads Checking — never Unknown - Open-in-new-window works for relay hosts: a new IPC command boots the local UI with the host id injected and the renderer picks the transport; the app render holds on the relay restore so the splash shows instead of a transient auth screen (10s safety valve) - Relay host control socket gained protocol-level keepalive: a missed pong window terminates and reconnects, so the relay can no longer hold a ghost registration that leaves every client tunnel hanging; the desktop relay probe also hard-times-out at 8s instead of hanging status flows - Services dropdown restyled with mobile-style cards: per-provider usage cards, per-host instance cards with a selected highlight and a toned status line, MCP servers grouped in a card
This commit is contained in:
@@ -480,22 +480,23 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="py-2">
|
||||
{rateLimitGroups.map((group, index) => {
|
||||
{/* One elevated card per provider (same card language as the mobile
|
||||
usage popover) instead of a flat run of divider-separated rows. */}
|
||||
<div className="space-y-2 px-3 py-2.5">
|
||||
{rateLimitGroups.map((group) => {
|
||||
const providerExpandedFamilies = expandedFamilies[group.providerId] ?? [];
|
||||
return (
|
||||
<React.Fragment key={group.providerId}>
|
||||
{index > 0 ? <div className="mx-4 my-2 border-t border-[var(--interactive-border)]" /> : null}
|
||||
<div className="flex items-center gap-2 px-4 py-2">
|
||||
<div key={group.providerId} className="min-w-0 rounded-xl bg-[var(--surface-muted)] p-3">
|
||||
<div className="flex items-center gap-2 pb-2">
|
||||
<ProviderLogo providerId={group.providerId} className="h-4 w-4" />
|
||||
<span className="typography-ui-label font-medium text-foreground">{group.providerName}</span>
|
||||
</div>
|
||||
{group.entries.length === 0 && (!group.modelFamilies || group.modelFamilies.length === 0) ? (
|
||||
<div className="px-4 pb-2">
|
||||
<span className="typography-ui-label text-muted-foreground">{group.error ?? t('header.services.noRateLimitsReported')}</span>
|
||||
<div>
|
||||
<span className="typography-ui-label text-muted-foreground">{group.error ?? t('header.services.noRateLimitsReported')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 px-4 pb-2">
|
||||
<div className="space-y-3">
|
||||
{group.entries.map(([label, window]) => {
|
||||
const displayPercent = quotaDisplayMode === 'remaining' ? window.remainingPercent : window.usedPercent;
|
||||
const paceInfo = calculatePace(window.usedPercent, window.resetAt, window.windowSeconds, label);
|
||||
@@ -584,7 +585,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user