fix: session pipeline tools survive another tab taking the panel, reopening the editor on call

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-10-01 14:13:33 +02:00
co-authored by Claude Opus 5.5
parent 0b4fafc02e
commit 46ec2995cc
3 changed files with 38 additions and 4 deletions
@@ -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<string>()
readonly isOpen = $derived(chatState.size > 0)
savedSize = $state<number>(0)
instructions = $state<string>('')
@@ -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<PipelineAIChatHelpers | undefined> => {
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()
@@ -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="<folder>") 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="<folder>") 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.
@@ -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<DeployResult> {
const key = normalizePipelineFolder(folder)
const target = previewTargetForSessionTarget('pipeline', key)