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
18 changes: 16 additions & 2 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import { checkAuthRequired, forceLogout, registerDisconnect } from "$lib/auth";
import { chatStream } from "$lib/chatStreamStore.svelte";
import { Button } from "$lib/components/ui/button";
import { connectionManager } from "$lib/connectionStore.svelte";
import { extensions, fetchBadgesForEnabledExtensions, fetchExtensions } from "$lib/extensionStore";
import { extensionNavItems, extensions, fetchBadgesForEnabledExtensions, fetchExtensions } from "$lib/extensionStore";
import { resolveIcon } from "$lib/iconRegistry";
import { readState } from "$lib/readState.svelte";
import { automationStyle } from "$lib/utils";
import { workflowStore } from "$lib/workflowRunStore.svelte";
Expand Down Expand Up @@ -132,7 +133,8 @@ onDestroy(() => {

let isLoginPage = $derived($pathname === "/login");
let isChat = $derived($pathname === "/" || $pathname === "/chat" || $pathname.startsWith("/chat/"));
let isFullHeight = $derived(isChat);
let isExtensionPage = $derived($pathname.startsWith("/ext-page/"));
let isFullHeight = $derived(isChat || isExtensionPage);

// Connect when navigating away from login (after successful login),
// disconnect when navigating to login (logout).
Expand All @@ -154,6 +156,12 @@ $effect(() => {
let showConnectionError = $derived(!$connected && !$hasConnected && !isLoginPage && !initialGracePeriod);

let hasUnreadChats = $derived(chatStream.conversations.some((c) => readState.isUnread(c.id, c.updatedAt)));

/** Current extension nav item (when on an extension page). */
let currentExtNavItem = $derived.by(() => {
if (!isExtensionPage) return null;
return $extensionNavItems.find((item) => $pathname === item.route || $pathname.startsWith(`${item.route}/`)) ?? null;
});
</script>

<Tooltip.Provider delayDuration={0}>
Expand Down Expand Up @@ -198,6 +206,12 @@ let hasUnreadChats = $derived(chatStream.conversations.some((c) => readState.isU
{:else if $pathname === "/mcp"}
<PlugIcon class="w-6 h-6 {automationStyle('mcp').color}" aria-hidden="true" />
MCP Servers
{:else if isExtensionPage && currentExtNavItem}
{@const IconComponent = resolveIcon(currentExtNavItem.icon)}
{#if IconComponent}
<IconComponent class="w-6 h-6 {currentExtNavItem.iconColor ?? ''}" aria-hidden="true" />
{/if}
{currentExtNavItem.label}
{:else}
<TrayIcon class="w-6 h-6" aria-hidden="true" />
Job Queues
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/iconRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import FlowArrowIcon from "phosphor-svelte/lib/FlowArrowIcon";
import GearIcon from "phosphor-svelte/lib/GearIcon";
import LinkIcon from "phosphor-svelte/lib/LinkIcon";
import PlugIcon from "phosphor-svelte/lib/PlugIcon";
import ReceiptIcon from "phosphor-svelte/lib/ReceiptIcon";
import TrayIcon from "phosphor-svelte/lib/TrayIcon";
import type { Component } from "svelte";

Expand All @@ -17,6 +18,7 @@ export const iconRegistry: Record<string, Component> = {
GearIcon,
LinkIcon,
PlugIcon,
ReceiptIcon,
TrayIcon,
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const { p, navigate, isActive, route } = createRouter({
"/workflows": () => import("./routes/WorkflowsPage.svelte"),
"/workflows/:name": () => import("./routes/WorkflowDetailPage.svelte"),
"/workflows/:name/runs/:runId": () => import("./routes/WorkflowRunPage.svelte"),
"/ext-page/:extensionName": () => import("./routes/ExtensionPage.svelte"),

hooks: {
async beforeLoad(context) {
Expand Down
64 changes: 64 additions & 0 deletions frontend/src/routes/ExtensionPage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import { authFetch, getToken } from "$lib/auth";
import LoadingIndicator from "$lib/components/LoadingIndicator.svelte";
import { route } from "../router";

// Extract extension name from route params
// Route: /ext-page/:extensionName
let extensionName = $derived(route.params.extensionName ?? "");

let htmlContent: string | null = $state(null);
let error: string | null = $state(null);
let loading = $state(true);

$effect(() => {
if (extensionName) {
loadPage(extensionName);
}
});

async function loadPage(name: string) {
loading = true;
error = null;
htmlContent = null;
try {
const res = await authFetch(`/ext/${name}/ui`);
if (!res.ok) {
error = `Failed to load extension page: ${res.status} ${res.statusText}`;
return;
}
let html = await res.text();
// Inject auth token so the iframe page can make authenticated API calls
const token = getToken();
if (token) {
// Build the script injection without a literal closing script tag in source
const scriptOpen = "<" + "script>";
const scriptClose = "</" + "script>";
const tokenScript = `${scriptOpen}window.__palimToken = "${token}";${scriptClose}`;
html = html.replace("<head>", `<head>${tokenScript}`);
}
htmlContent = html;
} catch (err) {
error = err instanceof Error ? err.message : "Failed to load page";
} finally {
loading = false;
}
}
</script>

{#if loading}
<div class="flex items-center justify-center h-full">
<LoadingIndicator message="Loading extension page..." />
</div>
{:else if error}
<div class="flex items-center justify-center h-full">
<p class="text-sm text-destructive">{error}</p>
</div>
{:else if htmlContent}
<iframe
srcdoc={htmlContent}
class="w-full h-full border-0"
sandbox="allow-scripts allow-same-origin allow-forms"
title="Extension page: {extensionName}"
></iframe>
{/if}
Loading