diff --git a/apps/web/src/global-routines.ts b/apps/web/src/global-routines.ts index f564ebe63..402cc1f30 100644 --- a/apps/web/src/global-routines.ts +++ b/apps/web/src/global-routines.ts @@ -9,6 +9,7 @@ import { describeApiError } from "@/lib/api-query"; import type { APIQuery } from "@/lib/api-query"; import { useBench } from "./bench-context"; +import { useFromBench } from "./shell/page-crumbs"; import { WORKFLOWS_PATH_PREFIX } from "./path-ids"; import { tenantKeys } from "./query-client"; import { @@ -30,7 +31,7 @@ export function routineDetailPath(definitionId: string): string { return `${WORKFLOWS_PATH_PREFIX}/${encodeURIComponent(definitionId)}`; } -function useMemberBenches(): { +function useMemberBenches(fromBench: string | null): { readonly kind: "loading" | "ready"; readonly benches: readonly { tenantId: string; tenantName: string }[]; } { @@ -42,12 +43,15 @@ function useMemberBenches(): { () => benchMemberships.map((m) => ({ tenantId: m.tenantId, tenantName: m.tenantName })), [benchMemberships], ); + if (fromBench !== null) { + return { kind: "ready", benches: [{ tenantId: fromBench, tenantName: "" }] }; + } if (memberships.kind !== "ready") return { kind: "loading", benches: [] }; return { kind: "ready", benches }; } export function useGlobalRoutines(): APIQuery { - const { kind: benchesKind, benches } = useMemberBenches(); + const { kind: benchesKind, benches } = useMemberBenches(useFromBench()); const results = useQueries({ queries: benches.map((bench) => ({ queryKey: [...tenantKeys.routines(bench.tenantId), ROUTINES_QUERY_SCOPE], diff --git a/apps/web/src/pages/insights-page.tsx b/apps/web/src/pages/insights-page.tsx index 9a0e5b4b3..bb837b5ed 100644 --- a/apps/web/src/pages/insights-page.tsx +++ b/apps/web/src/pages/insights-page.tsx @@ -53,6 +53,7 @@ import { useNavigate } from "../navigation"; import { tenantKeys } from "../query-client"; import { useAPIQuery } from "../api"; import { INSIGHTS_PATH_PREFIX, INSIGHTS_RUNS_PATH } from "../path-ids"; +import { benchLink, useFromBench } from "../shell/page-crumbs"; import { StageTopBar } from "../shell/stage-top-bar"; import { listScheduledWorkflows, @@ -745,7 +746,11 @@ function InsightsWorkbenchPage({ } export function InsightsRoute({ path }: { readonly path?: string }) { - const { selectedTenantId } = useBench(); + const { selectedTenantId: benchTenantId } = useBench(); + // A run opened from a workbench's Insights carries `from=`, so it reads + // that workbench's tenant rather than the bench. + const fromBench = useFromBench(); + const selectedTenantId = fromBench ?? benchTenantId; const navigate = useNavigate(); const currentPath = path ?? (typeof window !== "undefined" ? window.location.pathname : INSIGHTS_PATH_PREFIX); @@ -775,8 +780,10 @@ export function InsightsRoute({ path }: { readonly path?: string }) { return ( navigate(`${INSIGHTS_RUNS_PATH}/${encodeURIComponent(id)}`)} + benchTenantId={benchTenantId} + onOpenRun={(id) => + navigate(benchLink(`${INSIGHTS_RUNS_PATH}/${encodeURIComponent(id)}`, workbenchId)) + } /> ); } diff --git a/apps/web/src/pages/library-page.tsx b/apps/web/src/pages/library-page.tsx index 3609bbe01..6bfc56f2e 100644 --- a/apps/web/src/pages/library-page.tsx +++ b/apps/web/src/pages/library-page.tsx @@ -57,6 +57,7 @@ import { Link } from "../navigation"; import { ARTIFACTS_PATH_PREFIX } from "../path-ids"; import { tenantKeys } from "../query-client"; import { useBenchActivity } from "../shell/bench-activity"; +import { useFromBench } from "../shell/page-crumbs"; import { artifactUploadToast, copyArtifactLinks, @@ -595,7 +596,9 @@ export function LibraryRoute({ path }: { readonly path: string }) { // recorded one — resolved via the same activity listing other // bench-scoped surfaces already fetch. const activity = useBenchActivity(selectedTenantId); - const lastWorkbenchId = selectedTenantId === null ? null : readLastWorkbenchId(selectedTenantId); + const fromBench = useFromBench(); + const lastWorkbenchId = + fromBench ?? (selectedTenantId === null ? null : readLastWorkbenchId(selectedTenantId)); const workbenchScope = activity.kind === "ready" ? resolveLibraryWorkbenchScope(activity.workbenches, lastWorkbenchId) diff --git a/apps/web/src/pages/routines-page.tsx b/apps/web/src/pages/routines-page.tsx index b466aa99f..5cd983242 100644 --- a/apps/web/src/pages/routines-page.tsx +++ b/apps/web/src/pages/routines-page.tsx @@ -19,6 +19,7 @@ import { useGlobalRoutines, useRoutineActions } from "../global-routines"; import type { GlobalRoutineRow } from "../global-routines"; import { routineDetailPath } from "../global-routines"; import { Link } from "../navigation"; +import { benchLink, useFromBench } from "../shell/page-crumbs"; import { StageTopBar } from "../shell/stage-top-bar"; export type { GlobalRoutineRow } from "../global-routines"; @@ -39,6 +40,7 @@ export function GlobalRoutinesList({ readonly onToggleEnabled: (row: GlobalRoutineRow, enabled: boolean) => void; readonly onRunNow: (row: GlobalRoutineRow) => Promise; }) { + const fromBench = useFromBench(); if (rows.length === 0) { return ( {row.definition.name} - {row.tenantName} + {row.tenantName === "" ? null : ( + {row.tenantName} + )} diff --git a/apps/web/src/pages/tools-page.tsx b/apps/web/src/pages/tools-page.tsx index ede402588..8a88dde05 100644 --- a/apps/web/src/pages/tools-page.tsx +++ b/apps/web/src/pages/tools-page.tsx @@ -34,6 +34,7 @@ import { } from "../tools/mcp-servers-query"; import { useDeployedToolPackages } from "../tools/deployed-tool-packages"; import { useBench } from "../bench-context"; +import { useFromBench } from "../shell/page-crumbs"; import { StageTopBar } from "../shell/stage-top-bar"; import { ConfirmButton } from "../components/confirm-button"; @@ -343,6 +344,7 @@ export function ToolsPage({ tenantId }: { readonly tenantId: string | null }) { // A thin adapter that resolves which workbench's registry is listed. export function ToolsRoute() { const { selectedTenantId } = useBench(); + const fromBench = useFromBench(); - return ; + return ; } diff --git a/apps/web/src/router-store.ts b/apps/web/src/router-store.ts index c9bc3c0be..d52d55aac 100644 --- a/apps/web/src/router-store.ts +++ b/apps/web/src/router-store.ts @@ -10,13 +10,16 @@ function canonicalPath(pathname: string): string { const listeners = new Set<() => void>(); let currentPath = canonicalPath(window.location.pathname); +let currentSearch = window.location.search; if (currentPath !== window.location.pathname) { window.history.replaceState(null, "", currentPath); } function setPath(next: string): void { - if (next === currentPath) return; + const search = window.location.search; + if (next === currentPath && search === currentSearch) return; currentPath = next; + currentSearch = search; // Published on a microtask so a render-phase `navigateTo` (a forwarding // route resolving its target) never updates a subscriber mid-render. queueMicrotask(() => { @@ -49,7 +52,7 @@ export function navigateTo(to: string): void { // A hop to the path already showing is a no-op, which is what makes a // render-phase `navigate` safe to run twice (StrictMode, a re-render): // it can never stack duplicate history entries. - if (canonical === currentPath) return; + if (canonical === currentPath && url.search === currentSearch) return; window.history.pushState(null, "", canonical === url.pathname ? to : canonical); setPath(canonical); } diff --git a/apps/web/src/shell/page-crumbs.tsx b/apps/web/src/shell/page-crumbs.tsx index d76955dd6..6b9267e99 100644 --- a/apps/web/src/shell/page-crumbs.tsx +++ b/apps/web/src/shell/page-crumbs.tsx @@ -26,11 +26,18 @@ export function benchLink(path: string, benchId: string | null): string { export type PageCrumb = { readonly label: string; readonly href?: string }; -function useFromWorkbench(): { readonly id: string; readonly name: string | null } | null { +/** The workbench tenant id named by `?from=`, or null. Full-page views scope + * their data to it instead of the last-opened workbench. */ +export function useFromBench(): string | null { // The path store re-renders this on navigation; it keeps the pathname // only, so the query string is read fresh. useSyncExternalStore(subscribeToPath, getPath); const id = new URLSearchParams(window.location.search).get(FROM_PARAM); + return id === null || id === "" ? null : id; +} + +function useFromWorkbench(): { readonly id: string; readonly name: string | null } | null { + const id = useFromBench(); const { selectedTenantId } = useBench(); const tenantId = selectedTenantId ?? ""; const workbenches = useQuery({