fix(sessions): show the panel on a pinned handover, keep the eye alive after a stop

Both regressions from the previous round's fixes.

Re-pointing a pinned viewer did neither of the things `open` does besides
re-targeting: a collapsed panel stayed collapsed while the call told the reader
the page was open.

The stop check sat on the method the card's eye calls too. The controller is
per-turn and stays aborted once stopped, so the eye went dead on every card in
a chat where any turn had been stopped. The tools now go through a guarded
variant; the reader's own way in carries no turn check, because reopening a
page they closed has nothing to do with a turn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-01 09:46:07 -07:00
co-authored by Claude Opus 5
parent 904abda69b
commit cb0cdeccf1
2 changed files with 24 additions and 10 deletions
@@ -2102,19 +2102,32 @@ export class AIChatManager implements ChatViewHost {
* form already there. The tool call ends at the open — the run belongs to the reader, who
* starts it from that page with its scheduling, tag and version controls in reach.
*
* False where there is no page to show (no session host) and where the turn was stopped
* while the tool read the item's schema: opening then would act on a turn the reader
* ended, and would write a form onto a card the stop already settled, which the card would
* read as `Opened`. Declining hands the call on to `requestRunArgs`, whose own stop guard
* settles it as cancelled. */
* This is the reader's own way in — the card's eye, reopening a page they closed — so it
* carries no turn check. False means there is no session to show it in. The tool's way in
* is {@link #handOverDeployedRun}, which additionally refuses after a stop. */
openDeployedRunPage = (a: {
kind: 'script' | 'flow'
path: string
summary: string
args: Record<string, any>
}): boolean => this.openDeployedRunPageHandler?.(a) ?? false
/** What the run tools call. Opening after a stop would act on a turn the reader ended, and
* would write a form onto a card the stop already settled — which the card then reads as
* `Opened`, hiding the cancellation. Declining hands the call on to `requestRunArgs`,
* whose own stop guard settles it as cancelled.
*
* The controller is per-turn and stays aborted once stopped, so this check belongs here
* rather than on `openDeployedRunPage`: the reader's eye button must keep working on a
* card whose turn was stopped long ago. */
#handOverDeployedRun = (a: {
kind: 'script' | 'flow'
path: string
summary: string
args: Record<string, any>
}): boolean => {
if (this.abortController?.signal.aborted) return false
return this.openDeployedRunPageHandler?.(a) ?? false
return this.openDeployedRunPage(a)
}
handleRunFormSubmit = (toolId: string, args: Record<string, any>): boolean => {
@@ -4177,7 +4190,7 @@ export class AIChatManager implements ChatViewHost {
requestUserQuestion: this.requestUserQuestion,
requestRunArgs: this.requestRunArgs,
markRunFormStarted: this.markRunFormStarted,
openDeployedRunPage: this.openDeployedRunPage,
openDeployedRunPage: this.#handOverDeployedRun,
onItemModified: (kind, path) => this.recordModifiedItem(kind, path),
onItemDeployed: (kind, from, to) => void this.renameModifiedItem(kind, from, to),
onItemDiscarded: (kind, path) => void this.removeModifiedItem(kind, path),
@@ -639,10 +639,11 @@ function createRuntime(session: Session): SessionRuntime {
})
if (pinned) {
previewTabs.navigate(viewTarget, pinned.id)
// `navigate` re-points without bringing the tab forward, and the call is about to
// tell the reader the page is open — which it would not be, if they had switched
// away from it before asking.
// `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(pinned.id)
previewTabs.setCollapsed(false)
} else {
previewTabs.open(viewTarget)
}