diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 25179abe5d..f4abb93830 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -507,6 +507,11 @@ export class AIChatManager implements ChatViewHost { // awaits this so the model's next build_pipeline_node call can't race ahead of // the async canvas mount and hit "Unknown tool call". #pipelineHelpersWaiters = new Set<(helpers: PipelineAIChatHelpers) => void>() + // A session's pipeline editor unmounts whenever another tab takes the preview + // panel. Its tools stay offered for every folder the session opened, and a call + // reopens the tab through this, so a page shown in between costs no failed call. + #pipelineReopener: ((folder: string) => void) | undefined = undefined + #openedPipelineFolders = new Set() readonly isOpen = $derived(chatState.size > 0) savedSize = $state(0) instructions = $state('') @@ -2504,8 +2509,27 @@ export class AIChatManager implements ChatViewHost { // Public because it is purely local, unlike `changeMode(GLOBAL)`, which also // fires the three network refreshes. + setPipelineReopener = (reopen: (folder: string) => void) => { + this.#pipelineReopener = reopen + } + + #pipelineFolders = (): string[] => [ + ...new Set([ + ...[...this.#pipelineEditors].map((p) => p.getFolder()), + ...(this.#pipelineReopener ? this.#openedPipelineFolders : []) + ]) + ] + + #ensurePipeline = async (folder: string): Promise => { + const find = () => [...this.#pipelineEditors].find((p) => p.getFolder() === folder) + const live = find() + if (live || !this.#pipelineReopener) return live + this.#pipelineReopener(folder) + return (await this.waitForPipelineHelpers(folder)) ? find() : undefined + } + configureGlobalMode = () => { - const hasPipelines = this.#pipelineEditors.size > 0 + const hasPipelines = this.#pipelineFolders().length > 0 const opts = this.globalAssemblyOpts() const baseHelpers: GlobalToolHelpers = { // A session targets its own fixed (possibly forked) workspace, so capture it for @@ -2538,7 +2562,12 @@ export class AIChatManager implements ChatViewHost { } this.#assembledTools = assembleGlobalTools(opts) this.helpers = hasPipelines - ? { ...baseHelpers, pipelines: () => [...this.#pipelineEditors] } + ? { + ...baseHelpers, + pipelines: () => [...this.#pipelineEditors], + pipelineFolders: this.#pipelineFolders, + ensurePipeline: this.#ensurePipeline + } : baseHelpers this.systemMessage = assembleGlobalSystemMessage(getCustomPromptParts(AIMode.GLOBAL), opts) this.syncArtifactsSession() @@ -2552,7 +2581,7 @@ export class AIChatManager implements ChatViewHost { mcpServers: this.mcpServers, access: this.sessionAccess, sessionContext: this.sessionContextResolver?.(), - pipelineFolders: [...new Set([...this.#pipelineEditors].map((p) => p.getFolder()))] + pipelineFolders: this.#pipelineFolders() }) // Fetch the workspace's AI skills and, if GLOBAL mode is still active, rebuild @@ -4885,6 +4914,7 @@ export class AIChatManager implements ChatViewHost { // Returns a cleanup that tears the registration back down. setPipelineHelpers = (pipelineHelpers: PipelineAIChatHelpers) => { this.#pipelineEditors.add(pipelineHelpers) + this.#openedPipelineFolders.add(pipelineHelpers.getFolder()) untrack(() => { if (this.mode === AIMode.GLOBAL) { this.configureGlobalMode() diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index b714636875..8856877d9c 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -1477,7 +1477,7 @@ ${pipelineBullet}` canWriteDraft, ` - After writing or substantially editing a script / flow / app draft, show it via open_preview(kind, path) so the user sees the editor and live preview right next to the chat. First check whether it is already shown: if unsure, call get_preview_status. Only call open_preview (or offer to) when no preview is open or it is showing a different item — don't re-open a preview already showing the item you just edited. -- Building a data pipeline: call open_preview(kind="pipeline", path="") as the FIRST step, before creating any node — this opens the pipeline editor the user reviews in. path is the folder, not an item; an empty ${when(canCreateFolder, 'or not-yet-created ')}folder is fine${when(canCreateFolder, ' (create_folder first if needed, then open it)')}. Opening it adds build_pipeline_node / edit_pipeline_node to your tools from the next step on — they are not in your tool list before open_preview returns, so call open_preview on its own, wait for its result, then build. Use ONLY those to add or change pipeline nodes, never write_script for a pipeline node — they apply directly as unsaved drafts on the canvas (no separate accept/reject step) that the user reviews and deploys. Do not write pipeline scripts without first opening the editor.` +- Building a data pipeline: call open_preview(kind="pipeline", path="") as the FIRST step, before creating any node — this opens the pipeline editor the user reviews in. path is the folder, not an item; an empty ${when(canCreateFolder, 'or not-yet-created ')}folder is fine${when(canCreateFolder, ' (create_folder first if needed, then open it)')}. Opening it adds build_pipeline_node / edit_pipeline_node to your tools from the next step on — they are not in your tool list before open_preview returns, so call open_preview on its own, wait for its result, then build. They then stay available for the rest of the session even while another tab (a page, another preview) is shown: a call brings the pipeline editor back by itself, so do not re-open it first. Use ONLY those to add or change pipeline nodes, never write_script for a pipeline node — they apply directly as unsaved drafts on the canvas (no separate accept/reject step) that the user reviews and deploys. Do not write pipeline scripts without first opening the editor.` )} - When debugging a running raw app, call get_app_runtime_logs to read the live preview's browser console output. It needs the raw app preview open (open_preview kind="raw_app"). - Writing an app file does not compile it: the open preview rebuilds it afterwards. After editing a raw app's frontend files with its preview open, call get_app_runtime_logs to check the build — when it failed, it returns the build errors (e.g. syntax or import errors) and bundler logs to fix. diff --git a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts index 958414dcd1..1aa7b7ef78 100644 --- a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts @@ -634,6 +634,10 @@ function createRuntime(session: Session): SessionRuntime { waiters.add(notify) }) } + manager.setPipelineReopener((folder) => { + const target = previewTargetForSessionTarget('pipeline', folder) + if (target) previewTabs.open(target) + }) async function deployPipeline(folder: string): Promise { const key = normalizePipelineFolder(folder) const target = previewTargetForSessionTarget('pipeline', key)