diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index a281b266da..2705653d6a 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -28,6 +28,7 @@ import JobLoader from './JobLoader.svelte' import JobProgressBar from '$lib/components/jobs/JobProgressBar.svelte' import { createEventDispatcher, getContext, onDestroy, onMount, untrack } from 'svelte' + import { beforeNavigate } from '$app/navigation' import { Button } from './common' import SplitPanesWrapper from './splitPanes/SplitPanesWrapper.svelte' import WindmillIcon from './icons/WindmillIcon.svelte' @@ -1203,6 +1204,24 @@ } }) + // Preserve the chat across the /scripts/add → /scripts/edit/{path} promotion + // (and same-script reloads): ScriptEditor unmounts + remounts on those + // transitions, and without this its onDestroy would saveAndClear the + // SCRIPT-mode conversation the user is still actively having about this same + // script. + let preserveChatOnDestroy = $state(false) + beforeNavigate(({ to }) => { + const dest = to?.url.pathname ?? '' + if (!dest.startsWith('/scripts/edit/')) return + const destPath = dest.slice('/scripts/edit/'.length) + const currentPath = aiChatManager.scriptEditorOptions?.path + // !currentPath: on /scripts/add, dest is /scripts/edit/{path} (initial save). + // destPath === currentPath: same script (e.g. query change / reload). + if (!currentPath || destPath === currentPath) { + preserveChatOnDestroy = true + } + }) + onMount(async () => { await inferSchema(code, { applyInitialArgs: true }) // Retry once if the initial inference failed silently (e.g. transient WASM @@ -1211,7 +1230,12 @@ if (!validCode && code && lang) { await inferSchema(code, { applyInitialArgs: true }) } - aiChatManager.saveAndClear() + // The previous instance's onDestroy may have preserved the chat for + // intra-script-editor nav; in that case mode is still SCRIPT and the + // conversation is intact. Skip saveAndClear so we don't blow it away. + if (aiChatManager.mode !== AIMode.SCRIPT) { + aiChatManager.saveAndClear() + } aiChatManager.changeMode(AIMode.SCRIPT) }) @@ -1298,8 +1322,10 @@ aiChatManager.scriptEditorShowDiffMode = undefined aiChatManager.scriptEditorGetLintErrors = undefined aiChatManager.scriptEditorOptions = undefined - aiChatManager.saveAndClear() - aiChatManager.changeMode(AIMode.NAVIGATOR) + if (!preserveChatOnDestroy) { + aiChatManager.saveAndClear() + aiChatManager.changeMode(AIMode.NAVIGATOR) + } // Clean up debug mode if (debugMode) { stopDebugging() @@ -1357,9 +1383,7 @@ // width (Svelte wires a ResizeObserver for bind:clientWidth). let splitContainerWidth = $state(0) const TEST_PANE_MIN_PX = 400 - const testPaneMinPercent = $derived( - paneMinPercent(splitContainerWidth, TEST_PANE_MIN_PX) - ) + const testPaneMinPercent = $derived(paneMinPercent(splitContainerWidth, TEST_PANE_MIN_PX)) // Raw user-controlled test size (what the splitter wrote, or what the // toggle set). The size we actually pass to is clamped to the diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index 8121ca8b70..e81e018e93 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -21,7 +21,8 @@ import type { Modules } from './RawAppModules.svelte' import { isRunnableByName, isRunnableByPath } from '../apps/inputType' import { aiChatManager, AIMode } from '../copilot/chat/AIChatManager.svelte' - import { onMount, untrack } from 'svelte' + import { onDestroy, onMount, untrack } from 'svelte' + import { beforeNavigate } from '$app/navigation' import type { AppDatatableMetadata, LintResult, @@ -618,8 +619,30 @@ ) } + // Preserve the chat across the /apps_raw/add → /apps_raw/edit/{path} promotion + // (and same-app reloads): RawAppEditor unmounts + remounts on those + // transitions, and without this the fresh onMount's saveAndClear would blow + // away the APP-mode conversation the user is still actively having about this + // same app. + let preserveChatOnDestroy = $state(false) + beforeNavigate(({ to }) => { + const dest = to?.url.pathname ?? '' + if (!dest.startsWith('/apps_raw/edit/')) return + const destPath = dest.slice('/apps_raw/edit/'.length) + // !path: on /apps_raw/add, dest is /apps_raw/edit/{path} (initial deploy). + // destPath === path: same app. + if (!path || destPath === path) { + preserveChatOnDestroy = true + } + }) + onMount(() => { - aiChatManager.saveAndClear() + // The previous instance may have preserved the chat for intra-app-editor + // nav; in that case mode is still APP and the conversation is intact. Skip + // saveAndClear so we don't blow it away. + if (aiChatManager.mode !== AIMode.APP) { + aiChatManager.saveAndClear() + } aiChatManager.changeMode(AIMode.APP) rawAppLintStore.enable() loadSharedUi() @@ -645,6 +668,16 @@ } }) + onDestroy(() => { + // Cross-app / leave-editor navigation resets to NAVIGATOR; same-app / + // add→edit navigation keeps the chat (the fresh onMount detects mode is + // still APP and skips its clearing saveAndClear). + if (!preserveChatOnDestroy) { + aiChatManager.saveAndClear() + aiChatManager.changeMode(AIMode.NAVIGATOR) + } + }) + // Sync data with aiChatManager.datatableCreationPolicy (bidirectional) $effect(() => { // Read the current policy from aiChatManager @@ -1637,8 +1670,9 @@ title="Build failed" class="relative before:absolute before:inset-0 before:-z-10 before:rounded-md before:bg-surface before:content-['']" > -
{buildError}
+
{buildError}
{/if}