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 @@
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}