diff --git a/site/src/components/Button/Button.tsx b/site/src/components/Button/Button.tsx index ff5200edb5..c03f836358 100644 --- a/site/src/components/Button/Button.tsx +++ b/site/src/components/Button/Button.tsx @@ -12,7 +12,7 @@ import { cn } from "utils/cn"; const buttonVariants = cva( ` inline-flex items-center justify-center gap-1 whitespace-nowrap font-sans - border-solid rounded-md transition-colors + border-solid rounded-md transition-colors shrink-0 text-sm font-medium cursor-pointer no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link disabled:pointer-events-none disabled:text-content-disabled diff --git a/site/src/components/ScrollArea/ScrollArea.tsx b/site/src/components/ScrollArea/ScrollArea.tsx index e23718dacf..868cf47fc9 100644 --- a/site/src/components/ScrollArea/ScrollArea.tsx +++ b/site/src/components/ScrollArea/ScrollArea.tsx @@ -24,7 +24,7 @@ export const ScrollArea = React.forwardRef< )); ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; -const ScrollBar = React.forwardRef< +export const ScrollBar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, orientation = "vertical", ...props }, ref) => ( @@ -41,6 +41,6 @@ const ScrollBar = React.forwardRef< )} {...props} > - + )); diff --git a/site/src/modules/apps/useAppLink.ts b/site/src/modules/apps/useAppLink.ts index daad0d493e..bc51863b5a 100644 --- a/site/src/modules/apps/useAppLink.ts +++ b/site/src/modules/apps/useAppLink.ts @@ -20,10 +20,17 @@ type UseAppLinkParams = { agent: WorkspaceAgent; }; +type AppLink = { + href: string; + onClick: (e: React.MouseEvent) => void; + label: string; + hasToken: boolean; +}; + export const useAppLink = ( app: WorkspaceApp, { agent, workspace }: UseAppLinkParams, -) => { +): AppLink => { const label = app.display_name ?? app.slug; const { proxy } = useProxy(); const { data: apiKeyResponse } = useQuery({ diff --git a/site/src/modules/tasks/tasks.ts b/site/src/modules/tasks/tasks.ts index c48f5ec1c3..93c8ca4b4c 100644 --- a/site/src/modules/tasks/tasks.ts +++ b/site/src/modules/tasks/tasks.ts @@ -1,4 +1,8 @@ -import type { Workspace } from "api/typesGenerated"; +import type { + Workspace, + WorkspaceAgent, + WorkspaceApp, +} from "api/typesGenerated"; export const AI_PROMPT_PARAMETER_NAME = "AI Prompt"; @@ -6,3 +10,25 @@ export type Task = { workspace: Workspace; prompt: string; }; + +export type WorkspaceAppWithAgent = WorkspaceApp & { + agent: WorkspaceAgent; +}; + +export function getTaskApps(task: Task): WorkspaceAppWithAgent[] { + return ( + task.workspace.latest_build.resources + .flatMap((r) => r.agents ?? []) + .flatMap((agent) => + agent.apps.map((app) => ({ + ...app, + agent, + })), + ) + // The Chat UI app will be displayed in the sidebar, so we don't want to + // show it as a tab. + .filter( + (app) => app.id !== task.workspace.latest_build.ai_task_sidebar_app_id, + ) + ); +} diff --git a/site/src/pages/TaskPage/TaskAppIframe.tsx b/site/src/pages/TaskPage/TaskAppIframe.tsx index 4cea4d7d5e..e893af148f 100644 --- a/site/src/pages/TaskPage/TaskAppIframe.tsx +++ b/site/src/pages/TaskPage/TaskAppIframe.tsx @@ -1,4 +1,3 @@ -import type { WorkspaceApp } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { DropdownMenu, @@ -7,55 +6,42 @@ import { DropdownMenuTrigger, } from "components/DropdownMenu/DropdownMenu"; import { Spinner } from "components/Spinner/Spinner"; +import { useProxy } from "contexts/ProxyContext"; import { EllipsisVertical, ExternalLinkIcon, HouseIcon } from "lucide-react"; import { useAppLink } from "modules/apps/useAppLink"; -import type { Task } from "modules/tasks/tasks"; +import type { Task, WorkspaceAppWithAgent } from "modules/tasks/tasks"; import { type FC, useRef } from "react"; import { Link as RouterLink } from "react-router"; import { cn } from "utils/cn"; +import { TaskWildcardWarning } from "./TaskWildcardWarning"; type TaskAppIFrameProps = { task: Task; - app: WorkspaceApp; + app: WorkspaceAppWithAgent; active: boolean; - pathname?: string; }; export const TaskAppIFrame: FC = ({ task, app, active, - pathname, }) => { - const agent = task.workspace.latest_build.resources - .flatMap((r) => r.agents) - .filter((a) => !!a) - .find((a) => a.apps.some((a) => a.id === app.id)); - - if (!agent) { - throw new Error(`Agent for app ${app.id} not found in task workspace`); - } - const link = useAppLink(app, { - agent, + agent: app.agent, workspace: task.workspace, }); - - const appHref = (): string => { - try { - const url = new URL(link.href, location.href); - if (pathname) { - url.pathname = pathname; - } - return url.toString(); - } catch (err) { - console.warn(`Failed to parse URL ${link.href} for app ${app.id}`, err); - return link.href; - } - }; - + const proxy = useProxy(); const frameRef = useRef(null); - const frameSrc = appHref(); + const shouldDisplayWildcardWarning = + app.subdomain && !proxy.proxy?.preferredWildcardHostname; + + if (shouldDisplayWildcardWarning) { + return ( +
+ +
+ ); + } return (
@@ -67,7 +53,7 @@ export const TaskAppIFrame: FC = ({ onClick={(e) => { e.preventDefault(); if (frameRef.current?.contentWindow) { - frameRef.current.contentWindow.location.href = appHref(); + frameRef.current.contentWindow.location.href = link.href; } }} > @@ -88,7 +74,7 @@ export const TaskAppIFrame: FC = ({ - + Open app in new tab @@ -103,7 +89,7 @@ export const TaskAppIFrame: FC = ({ app.health === "unhealthy" ? (