fix: leave a busy tab alone when handing it a proposal

The re-point branch was written for Preview, which the reader presses on
purpose, and now also runs for every proposal of an item whose page is open.
Unasked for, two of the things it does are not wanted: `navigate` rebuilds the
tab's url without the version the reader pinned, and `select` with
`setCollapsed` pulls the panel off whatever tab they were reading.

So the automatic path declines a pinned tab outright, leaving the pin and the
call on the card, and moves the panel only when that tab is the one already on
screen. An explicit Preview press keeps the behaviour it had.

A hand-off still remounts the page, so text typed into its form is lost. That
needs the page to report unsent edits upward, and is left out.

Tests the routing gate: a deployed run is handed over, a test run and a
chat-enabled flow are not — neither has a page with a form that could answer
the call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-13 07:12:30 -07:00
co-authored by Claude Opus 5
parent 5ea8769efe
commit e5e741d97d
2 changed files with 66 additions and 4 deletions
@@ -330,6 +330,54 @@ describe('AIChatManager run form', () => {
})
})
// Only a deployed item has a page with a form to confirm on: a test run executes the
// draft being written, and a chat-enabled flow's page is a conversation. Routing either
// there would park the call on a page that can never answer it.
it('hands a deployed run to a page already open, and nothing else', async () => {
const manager = new AIChatManager()
const routed: any[] = []
manager.openRunOnDeployedPage = (a) => {
routed.push(a)
return true
}
void manager.requestRunArgs('call_run', {
path: 'f/a/b',
kind: 'run',
runnableKind: 'flow',
summary: 'Plain flow',
schema: {},
args: {}
})
void manager.requestRunArgs('call_test', {
path: 'f/a/b',
kind: 'test',
schema: {},
args: {}
})
void manager.requestRunArgs('call_chat', {
path: 'f/a/c',
kind: 'run',
conversational: true,
schema: {},
args: {}
})
expect(routed).toEqual([
{
toolCallId: 'call_run',
kind: 'flow',
path: 'f/a/b',
summary: 'Plain flow',
carries: true,
// Never opens a tab of its own: the reader did not ask for one here.
onlyIfAlreadyOpen: true
}
])
// Routed after the form is installed, so the page finds the call the moment it mounts.
expect(manager.isRunFormPending('call_run')).toBe(true)
})
// Stopping the turn is the form's other way out, and it settles cards through
// settledToolDisplay rather than through #settleRunForm.
it('settles a stopped form on what it held too', () => {
@@ -614,7 +614,11 @@ function createRuntime(session: Session): SessionRuntime {
return manager.pendingRunFormArgs(toolCallId) ?? {}
},
adopt: (jobId, args, schema) => manager.handleRunFormJob(toolCallId, jobId, args, schema),
decline: () => manager.handleRunFormCancel(toolCallId)
decline: () => manager.handleRunFormCancel(toolCallId),
// The same claim the card's own form takes, so the two surfaces of one call cannot
// both be mid-submit.
beginSubmit: () => manager.beginRunFormSubmit(toolCallId),
endSubmit: () => manager.endRunFormSubmit(toolCallId)
}
}
@@ -662,12 +666,22 @@ function createRuntime(session: Session): SessionRuntime {
return slot.kind === 'viewer' && slot.viewerKind === kind && slot.path === path
})
if (open) {
// A tab pinned to a version is one the reader put somewhere deliberately, and
// `navigate` below rebuilds its url without the pin. Worth it when the reader asked
// for this page; not worth silently moving them off a version they chose, so an
// unasked-for hand-off declines and leaves the call on the card.
if (onlyIfAlreadyOpen && new URL(open.url, 'http://x').searchParams.get('version')) {
return false
}
previewTabs.navigate(viewTarget, open.id)
// `navigate` re-points a tab in place and nothing more, while the call is about to
// tell the reader the page is open. `open` would have brought it forward and shown
// the panel; this path has to do both itself.
previewTabs.select(open.id)
previewTabs.setCollapsed(false)
// the panel; this path has to do both itself — except when nobody asked, where
// taking the panel off the tab the reader is on is the thing to avoid.
if (!onlyIfAlreadyOpen || previewTabs.activeId === open.id) {
previewTabs.select(open.id)
previewTabs.setCollapsed(false)
}
} else {
// Only the item's own page carries a form for this call, so a caller routing a call
// that was not asked to open anything gets a no rather than a new tab.