diff --git a/scripts/check-edit-route-parity.js b/scripts/check-edit-route-parity.js index edaf73bf3f9e..f7d80ec24943 100644 --- a/scripts/check-edit-route-parity.js +++ b/scripts/check-edit-route-parity.js @@ -16,11 +16,21 @@ const ROUTE_FILE_PATTERN = /^\+(page|layout|server)\.(svelte|ts)$/; const REPO_ROOT = fileURLToPath(new URL("..", import.meta.url)); +// The roots cover the two halves of an active edit session: the file-editor +// workspace and the dashboard-preview viz tree. Onboarding/deploy flows live +// outside these roots on both sides and are intentionally not parity-checked: +// - web-local: `(misc)/welcome`, `(misc)/deploy` +// - web-admin: `/-/edit/welcome` +// They render distinct UIs that don't share the editor's component composition, +// so divergence is expected. const LOCAL_ROOTS = [ "web-local/src/routes/(application)/(workspace)", "web-local/src/routes/(viz)", ]; -const ADMIN_ROOT = "web-admin/src/routes/[organization]/[project]/-/edit"; +const ADMIN_ROOTS = [ + "web-admin/src/routes/[organization]/[project]/-/edit/(workspace)", + "web-admin/src/routes/[organization]/[project]/-/edit/(viz)", +]; // Logical paths that exist only in web-local by design. Keep a short reason // comment on each entry so a future contributor can judge whether it's still @@ -28,7 +38,7 @@ const ADMIN_ROOT = "web-admin/src/routes/[organization]/[project]/-/edit"; const LOCAL_ONLY_ALLOWLIST = [ // Citation URL routes // TODO: ensure citations within the edit session get routed to the developer - // preview dashboards _within_ the edit session, not to the branch preview + // preview dashboards _within_ the edit session, not to the branch preview // dashboards _outside_ the edit session. "/-/ai/[conversationId]/message/[messageId]/+layout.ts", "/-/ai/[conversationId]/message/[messageId]/-/open/+page.ts", @@ -40,17 +50,10 @@ const LOCAL_ONLY_ALLOWLIST = [ "/dashboard/[name]/+page.ts", ]; -const ADMIN_ONLY_ALLOWLIST = [ - // We have a layout at the root on rill-dev, not under subpath like (application)/(workspace)/ or (viz)/ - "/+layout.ts", - - // Welcome is under (misc) in local. There will be a future PR that moves them to root. - "/welcome/+layout.svelte", - "/welcome/+layout.ts", - "/welcome/+page.svelte", - "/welcome/add-data/+page.svelte", - "/welcome/add-data/+page.ts", -]; +// Empty today: every route under ADMIN_ROOTS has a web-local counterpart. +// Add an entry (with a reason comment) if cloud ever needs an editor route +// that has no local equivalent. +const ADMIN_ONLY_ALLOWLIST = []; function walkRoutes(absRoot) { const results = []; @@ -101,7 +104,7 @@ function staleAllowlistEntries(allowlist, shouldExistIn) { function main() { const localRoutes = collect(LOCAL_ROOTS); - const adminRoutes = collect([ADMIN_ROOT]); + const adminRoutes = collect(ADMIN_ROOTS); const missingInAdmin = diff(localRoutes, adminRoutes, LOCAL_ONLY_ALLOWLIST); const missingInLocal = diff(adminRoutes, localRoutes, ADMIN_ONLY_ALLOWLIST); @@ -126,7 +129,7 @@ function main() { ); for (const p of missingInAdmin) console.error(` ${p}`); console.error( - `\nFix: either mirror each route under ${ADMIN_ROOT}/, or add the path to LOCAL_ONLY_ALLOWLIST in scripts/check-edit-route-parity.js with a reason.`, + `\nFix: either mirror each route under web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/ or (viz)/, or add the path to LOCAL_ONLY_ALLOWLIST in scripts/check-edit-route-parity.js with a reason.`, ); } diff --git a/web-admin/src/features/embeds/EmbedHeader.svelte b/web-admin/src/features/embeds/EmbedHeader.svelte index 65a1ba353601..397fbb9a1acc 100644 --- a/web-admin/src/features/embeds/EmbedHeader.svelte +++ b/web-admin/src/features/embeds/EmbedHeader.svelte @@ -6,6 +6,10 @@ import { featureFlags } from "@rilldata/web-common/features/feature-flags"; import LastRefreshedDate from "@rilldata/web-admin/features/dashboards/listing/LastRefreshedDate.svelte"; import ChatToggle from "@rilldata/web-common/features/chat/layouts/sidebar/ChatToggle.svelte"; + import { + dashboardChatActions, + dashboardChatOpen, + } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store"; import type { V1Resource, V1ResourceName, @@ -87,7 +91,7 @@ {#if showDashboardChat}
- +
{/if} diff --git a/web-admin/src/features/embeds/init-embed-public-api.ts b/web-admin/src/features/embeds/init-embed-public-api.ts index a255d744601a..ee453e386711 100644 --- a/web-admin/src/features/embeds/init-embed-public-api.ts +++ b/web-admin/src/features/embeds/init-embed-public-api.ts @@ -12,8 +12,8 @@ import { themeControl } from "@rilldata/web-common/features/themes/theme-control import { getEmbedThemeStoreInstance } from "@rilldata/web-common/features/embeds/embed-theme"; import { EmbedStore } from "@rilldata/web-common/features/embeds/embed-store"; import { - chatOpen, - sidebarActions, + dashboardChatActions, + dashboardChatOpen, } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store"; const STATE_CHANGE_THROTTLE_TIMEOUT = 200; @@ -91,7 +91,7 @@ export default function initEmbedPublicAPI(): () => void { }); registerRPCMethod("getAiPane", () => { - return { open: get(chatOpen) }; + return { open: get(dashboardChatOpen) }; }); registerRPCMethod("setAiPane", (open: boolean) => { @@ -99,9 +99,9 @@ export default function initEmbedPublicAPI(): () => void { throw new Error("Expected open to be a boolean"); } if (open) { - sidebarActions.openChat(); + dashboardChatActions.openChat(); } else { - sidebarActions.closeChat(); + dashboardChatActions.closeChat(); } return true; }); @@ -149,7 +149,7 @@ export default function initEmbedPublicAPI(): () => void { AI_PANE_CHANGE_THROTTLE_TIMEOUT, AI_PANE_CHANGE_THROTTLE_TIMEOUT, ); - const aiPaneUnsubscribe = chatOpen.subscribe((isOpen) => { + const aiPaneUnsubscribe = dashboardChatOpen.subscribe((isOpen) => { aiPaneChangeThrottler.throttle(() => { emitNotification("aiPaneChanged", { open: isOpen, diff --git a/web-admin/src/features/navigation/nav-utils.ts b/web-admin/src/features/navigation/nav-utils.ts index ffff0135a69e..af6a81fcb8ca 100644 --- a/web-admin/src/features/navigation/nav-utils.ts +++ b/web-admin/src/features/navigation/nav-utils.ts @@ -103,6 +103,18 @@ export function isEditPage({ route }: Pick): boolean { return !!route?.id?.startsWith("/[organization]/[project]/-/edit"); } +/** + * True when the page is the explore or canvas preview inside Cloud Rill + * Developer (`/-/edit/(viz)/{explore,canvas}/[name]`). `isMetricsExplorerPage` + * and `isCanvasDashboardPage` only match production routes, so this is the + * editor-side equivalent for surfaces that need to swap chat affordances. + */ +export function isEditDashboardPreviewPage({ + route, +}: Pick): boolean { + return !!route?.id?.startsWith("/[organization]/[project]/-/edit/(viz)/"); +} + export function isProjectRequestAccessPage(page: Page): boolean { return !!page.route.id?.startsWith( "/[organization]/[project]/-/request-access", diff --git a/web-admin/src/features/projects/ProjectHeader.svelte b/web-admin/src/features/projects/ProjectHeader.svelte index f15aaa8a73d3..ac20eea6f990 100644 --- a/web-admin/src/features/projects/ProjectHeader.svelte +++ b/web-admin/src/features/projects/ProjectHeader.svelte @@ -11,6 +11,12 @@ import type { PathOption } from "@rilldata/web-common/components/navigation/breadcrumbs/types"; import { useCanvas } from "@rilldata/web-common/features/canvas/selector"; import ChatToggle from "@rilldata/web-common/features/chat/layouts/sidebar/ChatToggle.svelte"; + import { + dashboardChatActions, + dashboardChatOpen, + developerChatActions, + developerChatOpen, + } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store"; import GlobalDimensionSearch from "@rilldata/web-common/features/dashboards/dimension-search/GlobalDimensionSearch.svelte"; import StateManagersProvider from "@rilldata/web-common/features/dashboards/state-managers/StateManagersProvider.svelte"; import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors"; @@ -35,6 +41,7 @@ } from "../navigation/breadcrumb-selectors"; import { isCanvasDashboardPage, + isEditDashboardPreviewPage, isMetricsExplorerPage, isProjectPage, isPublicURLPage, @@ -75,6 +82,8 @@ $: onCanvasDashboardPage = isCanvasDashboardPage($page); $: onPublicURLPage = isPublicURLPage($page); + $: onEditDashboardPreview = isEditDashboardPreviewPage($page); + $: activeBranch = extractBranchFromPath($page.url.pathname); $: loggedIn = !!$user.data?.user; @@ -195,8 +204,11 @@
{#if editContext} - {#if $developerChat} - + {#if $developerChat && !onEditDashboardPreview} + + {/if} + {#if $dashboardChat && onEditDashboardPreview} + {/if} {:else} @@ -237,8 +249,11 @@ {#if $dimensionSearch && ready} {/if} - {#if $dashboardChat && !onPublicURLPage} - + {#if $dashboardChat && !onPublicURLPage && !editContext} + {/if} {#if hasUserAccess} {/if} - {#if $dashboardChat && !onPublicURLPage} - + {#if $dashboardChat && !onPublicURLPage && !editContext} + {/if} {#if hasUserAccess} diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/canvas/[name]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(viz)/canvas/[name]/+page.svelte similarity index 53% rename from web-admin/src/routes/[organization]/[project]/-/edit/canvas/[name]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(viz)/canvas/[name]/+page.svelte index fa0a66392077..46c462432835 100644 --- a/web-admin/src/routes/[organization]/[project]/-/edit/canvas/[name]/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/edit/(viz)/canvas/[name]/+page.svelte @@ -1,6 +1,8 @@ -
+
diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/explore/[name]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(viz)/explore/[name]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/explore/[name]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(viz)/explore/[name]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/explore/[name]/+page.ts b/web-admin/src/routes/[organization]/[project]/-/edit/(viz)/explore/[name]/+page.ts similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/explore/[name]/+page.ts rename to web-admin/src/routes/[organization]/[project]/-/edit/(viz)/explore/[name]/+page.ts diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/+layout.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/+layout.svelte new file mode 100644 index 000000000000..0cd3eb0da997 --- /dev/null +++ b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/+layout.svelte @@ -0,0 +1,14 @@ + + +
+ +
+
+ +
+ +
+
diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/athena/[name]/[database]/[schema]/[table]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/athena/[name]/[database]/[schema]/[table]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/athena/[name]/[database]/[schema]/[table]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/athena/[name]/[database]/[schema]/[table]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/bigquery/[name]/[database]/[schema]/[table]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/bigquery/[name]/[database]/[schema]/[table]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/bigquery/[name]/[database]/[schema]/[table]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/bigquery/[name]/[database]/[schema]/[table]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/clickhouse/+page.ts b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/clickhouse/+page.ts similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/clickhouse/+page.ts rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/clickhouse/+page.ts diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/clickhouse/[name]/[database]/[table]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/clickhouse/[name]/[database]/[table]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/clickhouse/[name]/[database]/[table]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/clickhouse/[name]/[database]/[table]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/druid/+page.ts b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/druid/+page.ts similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/druid/+page.ts rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/druid/+page.ts diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/druid/[name]/[schema]/[table]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/druid/[name]/[schema]/[table]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/druid/[name]/[schema]/[table]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/druid/[name]/[schema]/[table]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/duckdb/+page.ts b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/duckdb/+page.ts similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/duckdb/+page.ts rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/duckdb/+page.ts diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/duckdb/[name]/[database]/[schema]/[table]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/duckdb/[name]/[database]/[schema]/[table]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/duckdb/[name]/[database]/[schema]/[table]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/duckdb/[name]/[database]/[schema]/[table]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/pinot/+page.ts b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/pinot/+page.ts similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/pinot/+page.ts rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/pinot/+page.ts diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/pinot/[name]/[table]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/pinot/[name]/[table]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/pinot/[name]/[table]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/pinot/[name]/[table]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/redshift/[name]/[database]/[schema]/[table]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/redshift/[name]/[database]/[schema]/[table]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/redshift/[name]/[database]/[schema]/[table]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/redshift/[name]/[database]/[schema]/[table]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/connector/snowflake/[name]/[database]/[schema]/[table]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/snowflake/[name]/[database]/[schema]/[table]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/connector/snowflake/[name]/[database]/[schema]/[table]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/connector/snowflake/[name]/[database]/[schema]/[table]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/files/[...file]/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/files/[...file]/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/files/[...file]/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/files/[...file]/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/files/[...file]/+page.ts b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/files/[...file]/+page.ts similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/files/[...file]/+page.ts rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/files/[...file]/+page.ts diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/graph/+page.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/graph/+page.svelte similarity index 100% rename from web-admin/src/routes/[organization]/[project]/-/edit/graph/+page.svelte rename to web-admin/src/routes/[organization]/[project]/-/edit/(workspace)/graph/+page.svelte diff --git a/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte b/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte index b700f9c84c84..3476bb0a5e52 100644 --- a/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/-/edit/+layout.svelte @@ -23,11 +23,9 @@ import CtaLayoutContainer from "@rilldata/web-common/components/calls-to-action/CTALayoutContainer.svelte"; import CtaMessage from "@rilldata/web-common/components/calls-to-action/CTAMessage.svelte"; import ErrorPage from "@rilldata/web-common/components/ErrorPage.svelte"; - import DeveloperChat from "@rilldata/web-common/features/chat/DeveloperChat.svelte"; import FileAndResourceWatcher from "@rilldata/web-common/features/entity-management/FileAndResourceWatcher.svelte"; import { themeControl } from "@rilldata/web-common/features/themes/theme-control"; import { editorRoutePrefix } from "@rilldata/web-common/layout/navigation/editor-routing"; - import Navigation from "@rilldata/web-common/layout/navigation/Navigation.svelte"; import RuntimeProvider from "@rilldata/web-common/runtime-client/v2/RuntimeProvider.svelte"; import { useQueryClient } from "@tanstack/svelte-query"; import { onDestroy } from "svelte"; @@ -185,18 +183,10 @@ {onBeforeReconnect} errorBody="Lost connection to the editing environment. Try ending the session and starting a new one." > -
- {#if !inProjectWelcomePage} - - - {/if} -
-
- -
- -
-
+ {#if !inProjectWelcomePage} + + {/if} + {/key} diff --git a/web-common/src/features/canvas/CanvasPreviewCTAs.svelte b/web-common/src/features/canvas/CanvasPreviewCTAs.svelte index 163726bf9087..7f397c1f68f2 100644 --- a/web-common/src/features/canvas/CanvasPreviewCTAs.svelte +++ b/web-common/src/features/canvas/CanvasPreviewCTAs.svelte @@ -5,6 +5,10 @@ import { featureFlags } from "../feature-flags"; import { getFileHref } from "../../layout/navigation/editor-routing"; import ChatToggle from "@rilldata/web-common/features/chat/layouts/sidebar/ChatToggle.svelte"; + import { + dashboardChatActions, + dashboardChatOpen, + } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store"; import ViewAsButton from "../dashboards/granular-access-policies/ViewAsButton.svelte"; import { useDashboardPolicyCheck, @@ -42,7 +46,7 @@ {/if} {#if $dashboardChat} - + {/if} {#if !$readOnly} diff --git a/web-common/src/features/canvas/ai-generation/generateCanvas.ts b/web-common/src/features/canvas/ai-generation/generateCanvas.ts index bccd444d42ff..0a1fc7d22517 100644 --- a/web-common/src/features/canvas/ai-generation/generateCanvas.ts +++ b/web-common/src/features/canvas/ai-generation/generateCanvas.ts @@ -1,6 +1,6 @@ import { getConversationManager } from "@rilldata/web-common/features/chat/core/conversation-manager"; import { ToolName } from "@rilldata/web-common/features/chat/core/types"; -import { sidebarActions } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store"; +import { developerChatActions } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store"; import { pollForFileCreation } from "@rilldata/web-common/features/entity-management/actions/actions.ts"; import { fileArtifacts } from "@rilldata/web-common/features/entity-management/file-artifacts"; import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors"; @@ -178,6 +178,7 @@ export async function createCanvasDashboardFromMetricsViewWithAgent( const conversationManager = getConversationManager(client, { conversationState: "browserStorage", agent: ToolName.DEVELOPER_AGENT, + surface: "developer", }); // Start a new conversation instead of continuing existing one @@ -191,7 +192,7 @@ export async function createCanvasDashboardFromMetricsViewWithAgent( generatingCanvas.set(true); // 4. Start the chat with the generation prompt - sidebarActions.startChat(prompt); + developerChatActions.startChat(prompt); // Wait for the stream to start async through the sidebar action. await waitUntil(() => get(currentConversation.isStreaming)); diff --git a/web-common/src/features/canvas/components/charts/custom-chart/chart-ai-agent.ts b/web-common/src/features/canvas/components/charts/custom-chart/chart-ai-agent.ts index c2ca4637ce39..6c76e03ab1b0 100644 --- a/web-common/src/features/canvas/components/charts/custom-chart/chart-ai-agent.ts +++ b/web-common/src/features/canvas/components/charts/custom-chart/chart-ai-agent.ts @@ -1,7 +1,7 @@ import type { Conversation } from "@rilldata/web-common/features/chat/core/conversation"; import { getConversationManager } from "@rilldata/web-common/features/chat/core/conversation-manager"; import { ToolName } from "@rilldata/web-common/features/chat/core/types"; -import { sidebarActions } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store"; +import { developerChatActions } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store"; import type { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { derived, get, type Readable } from "svelte/store"; import type { CustomChartComponent } from "./index"; @@ -39,6 +39,7 @@ export function sendToDevAgent( const conversationManager = getConversationManager(client, { conversationState: "browserStorage", agent: ToolName.DEVELOPER_AGENT, + surface: "developer", }); const existing = componentConversations.get(component.id); @@ -51,7 +52,7 @@ export function sendToDevAgent( } const fullPrompt = buildPrompt(component, userPrompt); - sidebarActions.startChat(fullPrompt); + developerChatActions.startChat(fullPrompt); // Track the conversation for this component so subsequent calls continue it const conversation = get(conversationManager.getCurrentConversation()); @@ -69,6 +70,7 @@ export function getAgentStreamingStore( const conversationManager = getConversationManager(client, { conversationState: "browserStorage", agent: ToolName.DEVELOPER_AGENT, + surface: "developer", }); return derived( diff --git a/web-common/src/features/chat/DashboardChat.svelte b/web-common/src/features/chat/DashboardChat.svelte index d2cb5a949af1..c6bc6b56a184 100644 --- a/web-common/src/features/chat/DashboardChat.svelte +++ b/web-common/src/features/chat/DashboardChat.svelte @@ -1,7 +1,10 @@ -{#if $dashboardChat && $chatOpen} +{#if $dashboardChat && $dashboardChatOpen} - + {/if} diff --git a/web-common/src/features/chat/DeveloperChat.svelte b/web-common/src/features/chat/DeveloperChat.svelte index 84c6af8f1d2e..36084664b001 100644 --- a/web-common/src/features/chat/DeveloperChat.svelte +++ b/web-common/src/features/chat/DeveloperChat.svelte @@ -1,12 +1,19 @@ -{#if $developerChat && $chatOpen} - +{#if $developerChat && $developerChatOpen} + {/if} diff --git a/web-common/src/features/chat/ExplainAndFixErrorButton.svelte b/web-common/src/features/chat/ExplainAndFixErrorButton.svelte index 6ae8b8ee649a..68370581a265 100644 --- a/web-common/src/features/chat/ExplainAndFixErrorButton.svelte +++ b/web-common/src/features/chat/ExplainAndFixErrorButton.svelte @@ -1,14 +1,14 @@ diff --git a/web-common/src/features/chat/core/conversation-manager.ts b/web-common/src/features/chat/core/conversation-manager.ts index 761a14d0aa00..2f7dec30b2df 100644 --- a/web-common/src/features/chat/core/conversation-manager.ts +++ b/web-common/src/features/chat/core/conversation-manager.ts @@ -13,29 +13,31 @@ import { URLConversationSelector, type ConversationSelector, } from "./conversation-selector"; +import type { ChatSurface } from "./types"; import { invalidateConversationsList, NEW_CONVERSATION_ID } from "./utils"; import { EmbedStore } from "@rilldata/web-common/features/embeds/embed-store.ts"; export type ConversationStateType = "url" | "browserStorage"; -export interface ConversationManagerOptions { - /** - * How conversation state should be managed and persisted - * - "url": Use URL parameters (for full-page chat with shareable URLs) - * - "browserStorage": Use session storage (for sidebar chat) - */ - conversationState: ConversationStateType; - /** - * The agent to use for conversations (e.g., "analyst_agent", "developer_agent") - */ - agent?: string; - /** - * Base path builder for URL-based conversation selectors. - * Only used when conversationState is "url". - * Defaults to `/${org}/${project}/-/ai` (web-admin pattern). - */ - basePath?: () => string; -} +/** + * Discriminated by `conversationState`: + * - "url": full-page chat with shareable URLs. Optional `basePath` overrides + * the default `/${org}/${project}/-/ai`. + * - "browserStorage": sidebar chat keyed in sessionStorage. `surface` is + * required and scopes the storage key so developer-surface conversations + * don't load against the dashboard surface. + */ +export type ConversationManagerOptions = + | { + conversationState: "url"; + agent?: string; + basePath?: () => string; + } + | { + conversationState: "browserStorage"; + agent?: string; + surface: ChatSurface; + }; /** * Manages chat state and conversation lifecycle. @@ -78,13 +80,20 @@ export class ConversationManager { break; case "browserStorage": this.conversationSelector = new BrowserStorageConversationSelector( + options.surface, + client.instanceId, EmbedStore.getInstance()?.externalUserId ?? null, ); break; - default: + default: { + // Exhaustiveness check: if a new variant is added to + // ConversationManagerOptions without a case, this assignment fails to + // compile. + const exhaustive: never = options; throw new Error( - `Unknown conversation storage type: ${options.conversationState}`, + `Unknown conversation manager options: ${JSON.stringify(exhaustive)}`, ); + } } } diff --git a/web-common/src/features/chat/core/conversation-selector.ts b/web-common/src/features/chat/core/conversation-selector.ts index 13cecc1aae4b..3bd21de9a91d 100644 --- a/web-common/src/features/chat/core/conversation-selector.ts +++ b/web-common/src/features/chat/core/conversation-selector.ts @@ -10,6 +10,7 @@ import { goto } from "$app/navigation"; import { page } from "$app/stores"; import { derived, get, type Readable, type Writable } from "svelte/store"; import { sessionStorageStore } from "../../../lib/store-utils/session-storage"; +import type { ChatSurface } from "./types"; import { NEW_CONVERSATION_ID } from "./utils"; // ============================================================================= @@ -120,21 +121,29 @@ export class BrowserStorageConversationSelector readonly isNewConversation: Readable; /** + * @param surface The AI surface ("developer" or "dashboard"). Scopes the + * sessionStorage key so a developer-surface conversation ID does not get + * loaded against the dashboard surface (within the same instance, e.g. dev + * runtime serving both workspace and viz preview). + * @param instanceId The runtime instance ID. Scopes the sessionStorage key + * so a stored conversation ID does not get loaded against a different + * runtime than the one that created it (e.g. a publish-spawned prod tab + * inheriting sessionStorage from the dev tab, or a dev runtime cycling to + * a new instance after hibernation/redeploy). * @param scopeId Optional namespace for the sessionStorage key. * Pass when the same browser tab may serve multiple users (e.g. embed with `external_user_id`) * This prevents conversation sharing between contexts that are meant to be different. * * Note that this is not really a data leak issue since it will be on the same browser tab, most probably for the same end user. */ - constructor(scopeId: string | null) { - // Create project-specific storage store based on current page params - const currentPage = get(page); - const organization = currentPage.params.organization || ""; - const project = currentPage.params.project || ""; - + constructor( + surface: ChatSurface, + instanceId: string, + scopeId: string | null, + ) { const scopePart = scopeId ? "-" + scopeId : ""; this.store = sessionStorageStore( - `sidebar-conversation-id-${organization}-${project}${scopePart}`, + `sidebar-conversation-id-${surface}-${instanceId}${scopePart}`, NEW_CONVERSATION_ID, ); diff --git a/web-common/src/features/chat/core/types.ts b/web-common/src/features/chat/core/types.ts index 7b2b07c35341..17bfe399ce02 100644 --- a/web-common/src/features/chat/core/types.ts +++ b/web-common/src/features/chat/core/types.ts @@ -64,6 +64,13 @@ export const ToolName = { // CHAT CONFIG // ============================================================================= +/** + * The two AI surfaces in a Rill workspace. Used to scope sidebar-chat sessionStorage + * (open state, conversation ID) so a Cloud Rill Developer (`/-/edit/...`) session + * does not leak into the production tab opened on Publish. + */ +export type ChatSurface = "developer" | "dashboard"; + export type ChatConfig = { agent: string; additionalContextStoreGetter: () => Readable< diff --git a/web-common/src/features/chat/layouts/sidebar/ChatToggle.svelte b/web-common/src/features/chat/layouts/sidebar/ChatToggle.svelte index 7b4f5a3cae22..8fb1fc98eaca 100644 --- a/web-common/src/features/chat/layouts/sidebar/ChatToggle.svelte +++ b/web-common/src/features/chat/layouts/sidebar/ChatToggle.svelte @@ -1,7 +1,11 @@ @@ -10,7 +14,7 @@ onkeydown={(e) => { if (e[isMac ? "metaKey" : "ctrlKey"] && e.key === "j") { e.preventDefault(); - sidebarActions.toggleChat(); + actions.toggleChat(); } }} /> @@ -22,8 +26,8 @@ {...props} compact type="secondary" - onClick={sidebarActions.toggleChat} - active={$chatOpen} + onClick={actions.toggleChat} + active={$open} > AI diff --git a/web-common/src/features/chat/layouts/sidebar/SidebarChat.svelte b/web-common/src/features/chat/layouts/sidebar/SidebarChat.svelte index a5bc185fdf06..a70c1f132846 100644 --- a/web-common/src/features/chat/layouts/sidebar/SidebarChat.svelte +++ b/web-common/src/features/chat/layouts/sidebar/SidebarChat.svelte @@ -9,13 +9,18 @@ import SidebarHeader from "./SidebarHeader.svelte"; import { SIDEBAR_DEFAULTS, - sidebarActions, sidebarWidth, + type ChatActions, } from "./sidebar-store"; - import type { ChatConfig } from "@rilldata/web-common/features/chat/core/types.ts"; + import type { + ChatConfig, + ChatSurface, + } from "@rilldata/web-common/features/chat/core/types.ts"; export let config: ChatConfig; + export let actions: ChatActions; + export let surface: ChatSurface; const runtimeClient = useRuntimeClient(); @@ -23,6 +28,7 @@ $: conversationManager = getConversationManager(runtimeClient, { conversationState: "browserStorage", agent: config.agent, + surface, }); let chatInputComponent: ChatInput; @@ -66,14 +72,14 @@ dimension={$sidebarWidth} direction="EW" side="left" - onUpdate={sidebarActions.updateSidebarWidth} + onUpdate={actions.updateSidebarWidth} />
diff --git a/web-common/src/features/chat/layouts/sidebar/sidebar-store.ts b/web-common/src/features/chat/layouts/sidebar/sidebar-store.ts index 139fa215837f..31b02dea98ac 100644 --- a/web-common/src/features/chat/layouts/sidebar/sidebar-store.ts +++ b/web-common/src/features/chat/layouts/sidebar/sidebar-store.ts @@ -1,7 +1,7 @@ import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus.ts"; import { localStorageStore } from "../../../../lib/store-utils/local-storage"; import { sessionStorageStore } from "../../../../lib/store-utils/session-storage"; -import { get, writable } from "svelte/store"; +import { get, writable, type Writable } from "svelte/store"; import { waitUntil } from "@rilldata/web-common/lib/waitUtils.ts"; // ============================================================================= @@ -19,8 +19,17 @@ export const SIDEBAR_DEFAULTS = { // SIDEBAR STORES // ============================================================================= -export const chatOpen = sessionStorageStore( - "chat-open", +// Per-surface open state. Keeping the developer and dashboard panels on +// independent keys means publishing from a Rill Developer tab does not flip +// the chat-open flag in the freshly opened production tab — Chromium clones +// sessionStorage when window.open inherits the opener context. +export const developerChatOpen = sessionStorageStore( + "chat-open-developer", + SIDEBAR_DEFAULTS.CHAT_OPEN, +); + +export const dashboardChatOpen = sessionStorageStore( + "chat-open-dashboard", SIDEBAR_DEFAULTS.CHAT_OPEN, ); @@ -35,31 +44,40 @@ export const sidebarWidth = localStorageStore( // SIDEBAR ACTIONS // ============================================================================= -export const sidebarActions = { - toggleChat(): void { - chatOpen.update((isOpen) => !isOpen); - }, - - openChat(): void { - chatOpen.set(true); - }, - - startChat(prompt: string): void { - chatOpen.set(true); - void waitUntil(() => get(chatMounted)).then(() => - eventBus.emit("start-chat", prompt), - ); - }, +export type ChatActions = { + toggleChat(): void; + openChat(): void; + closeChat(): void; + startChat(prompt: string): void; + updateSidebarWidth(width: number): void; +}; - closeChat(): void { - chatOpen.set(false); - }, +function createChatActions(open: Writable): ChatActions { + return { + toggleChat() { + open.update((isOpen) => !isOpen); + }, + openChat() { + open.set(true); + }, + closeChat() { + open.set(false); + }, + startChat(prompt: string) { + open.set(true); + void waitUntil(() => get(chatMounted)).then(() => + eventBus.emit("start-chat", prompt), + ); + }, + updateSidebarWidth(width: number) { + const constrainedWidth = Math.max( + SIDEBAR_DEFAULTS.MIN_SIDEBAR_WIDTH, + Math.min(SIDEBAR_DEFAULTS.MAX_SIDEBAR_WIDTH, width), + ); + sidebarWidth.set(constrainedWidth); + }, + }; +} - updateSidebarWidth(width: number): void { - const constrainedWidth = Math.max( - SIDEBAR_DEFAULTS.MIN_SIDEBAR_WIDTH, - Math.min(SIDEBAR_DEFAULTS.MAX_SIDEBAR_WIDTH, width), - ); - sidebarWidth.set(constrainedWidth); - }, -}; +export const developerChatActions = createChatActions(developerChatOpen); +export const dashboardChatActions = createChatActions(dashboardChatOpen); diff --git a/web-common/src/features/dashboards/time-series/measure-selection/measure-selection.ts b/web-common/src/features/dashboards/time-series/measure-selection/measure-selection.ts index d2064487fb21..8e4c08fd58f5 100644 --- a/web-common/src/features/dashboards/time-series/measure-selection/measure-selection.ts +++ b/web-common/src/features/dashboards/time-series/measure-selection/measure-selection.ts @@ -3,7 +3,7 @@ import { type InlineContext, convertContextToInlinePrompt, } from "@rilldata/web-common/features/chat/core/context/inline-context.ts"; -import { sidebarActions } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store.ts"; +import { dashboardChatActions } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store.ts"; import { get, writable } from "svelte/store"; import { featureFlags } from "@rilldata/web-common/features/feature-flags.ts"; import { getExploreNameStore } from "@rilldata/web-common/features/dashboards/nav-utils.ts"; @@ -96,7 +96,7 @@ export class MeasureSelection { `Explain what drives ${measureMention}, ${timeRangeMention}. ` + `Which visible dimensions have noticeably changed, as compared to other time windows?`; - sidebarActions.startChat(prompt); + dashboardChatActions.startChat(prompt); } public getEnabledStore() { diff --git a/web-common/src/features/explores/ExplorePreviewCTAs.svelte b/web-common/src/features/explores/ExplorePreviewCTAs.svelte index e8affd59341b..d8b5e46d785f 100644 --- a/web-common/src/features/explores/ExplorePreviewCTAs.svelte +++ b/web-common/src/features/explores/ExplorePreviewCTAs.svelte @@ -8,6 +8,10 @@ import { Button } from "../../components/button"; import { useRuntimeClient } from "../../runtime-client/v2"; import ChatToggle from "../chat/layouts/sidebar/ChatToggle.svelte"; + import { + dashboardChatActions, + dashboardChatOpen, + } from "../chat/layouts/sidebar/sidebar-store"; import ViewAsButton from "../dashboards/granular-access-policies/ViewAsButton.svelte"; import { useDashboardPolicyCheck, @@ -46,7 +50,7 @@ {/if} {#if $dashboardChat} - + {/if} {#if ready} diff --git a/web-common/src/features/sample-data/generate-sample-data.ts b/web-common/src/features/sample-data/generate-sample-data.ts index be2c8bb52862..4bc40531a779 100644 --- a/web-common/src/features/sample-data/generate-sample-data.ts +++ b/web-common/src/features/sample-data/generate-sample-data.ts @@ -5,7 +5,7 @@ import { get, writable } from "svelte/store"; import { EMPTY_PROJECT_TITLE } from "@rilldata/web-common/features/welcome/constants.ts"; import { overlay } from "@rilldata/web-common/layout/overlay-store.ts"; import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus.ts"; -import { sidebarActions } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store.ts"; +import { developerChatActions } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store.ts"; import { getConversationManager } from "@rilldata/web-common/features/chat/core/conversation-manager.ts"; import type { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { navigateToHome } from "@rilldata/web-common/layout/navigation/editor-routing"; @@ -49,13 +49,14 @@ export async function generateSampleData( const conversationManager = getConversationManager(client, { conversationState: "browserStorage", agent: ToolName.DEVELOPER_AGENT, + surface: "developer", }); // Continue with the current chat. We might want to revisit this based on feedback. const conversation = get(conversationManager.getCurrentConversation()); conversation.cancelStream(); - sidebarActions.startChat(userPrompt); + developerChatActions.startChat(userPrompt); // Wait for the stream to start async through the sidebar action. await waitUntil(() => get(conversation.isStreaming)); diff --git a/web-common/src/layout/ApplicationHeader.svelte b/web-common/src/layout/ApplicationHeader.svelte index ab60858a1cac..10d3f2732ce8 100644 --- a/web-common/src/layout/ApplicationHeader.svelte +++ b/web-common/src/layout/ApplicationHeader.svelte @@ -8,6 +8,10 @@ import LocalAvatarButton from "@rilldata/web-common/features/authentication/LocalAvatarButton.svelte"; import CanvasPreviewCTAs from "@rilldata/web-common/features/canvas/CanvasPreviewCTAs.svelte"; import ChatToggle from "@rilldata/web-common/features/chat/layouts/sidebar/ChatToggle.svelte"; + import { + developerChatActions, + developerChatOpen, + } from "@rilldata/web-common/features/chat/layouts/sidebar/sidebar-store"; import { getBreadcrumbOptions } from "@rilldata/web-common/features/dashboards/dashboard-utils"; import { useValidCanvases, @@ -127,7 +131,7 @@ {:else if route.id?.includes("canvas")} {:else if showDeveloperChat} - + {/if} {#if showDeployCTA}