mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 16:02:57 +00:00
feat: hand a proposed run to the page already showing it
A reader who declines a proposal and asks for different arguments still has that item's page open, and the next proposal belongs on it — but it was parked on the card until Preview was pressed again. `requestRunArgs` now routes the call to that page itself, through `openRunOnDeployedPage` with `onlyIfAlreadyOpen`: a tab already on the item is re-pointed, and anything else — a tab showing a run included — answers no and leaves the form on the card. The card keeps a working form while a page carries the call, rather than reporting that its inputs are elsewhere. Only the chat's own form in a tab is exclusive with it, since that one is the same draft and two mounts each reorder the schema in place; a page builds its form from a copy of the proposal and shares nothing. So the card folds its row instead of emptying it, and expanding it is how a run goes from the transcript. `isRunFormTabOpen` answers that narrower question, and the collapse moves into the row's default so pressing Preview and being routed here behave alike. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9f9e10f0c8
commit
aaa436d126
@@ -595,6 +595,10 @@ export class AIChatManager implements ChatViewHost {
|
||||
path: string
|
||||
summary: string
|
||||
carries: boolean
|
||||
/** Re-point a tab already on that page, and answer `false` rather than opening one.
|
||||
* For handing a call to a page the reader already has in front of them: a tab showing
|
||||
* something else, a run included, has no form to confirm this call on. */
|
||||
onlyIfAlreadyOpen?: boolean
|
||||
}) => boolean
|
||||
closeRunForm?: (toolCallId: string) => void
|
||||
/** Hands that tab from the form to the run it just started, in place: the tab keeps its
|
||||
@@ -605,11 +609,16 @@ export class AIChatManager implements ChatViewHost {
|
||||
workspace: string
|
||||
label: string
|
||||
}) => void
|
||||
/** Whether the panel holds this call's pending form. Answered off the session's tab list,
|
||||
* so it stays true while the user is on another tab, and per call rather than "the open
|
||||
* one". Read from a `$derived` — the reader subscribes to the tab list through the call.
|
||||
* The card hides its form on it, which is what keeps exactly one mounted per call. */
|
||||
/** Whether the panel holds this call's pending form, on either surface. Answered off the
|
||||
* session's tab list, so it stays true while the user is on another tab, and per call
|
||||
* rather than "the open one". Read from a `$derived` — the reader subscribes to the tab
|
||||
* list through the call. The card folds its row on it, and a started job takes over
|
||||
* whichever surface was waiting. */
|
||||
isRunFormInPreview?: (toolCallId: string) => boolean
|
||||
/** Whether the panel holds this call on the chat's own form, as opposed to on the item's
|
||||
* page. The card's form is exclusive with this one alone: it is the same draft, and two
|
||||
* mounts of it each reorder the schema SchemaForm edits in place. */
|
||||
isRunFormTabOpen?: (toolCallId: string) => boolean
|
||||
openArtifact?: (artifactId: string, name: string, version?: ArtifactVersionTarget) => void
|
||||
closeArtifact?: (artifactId: string) => void
|
||||
#loading = $state<boolean>(false)
|
||||
@@ -2064,9 +2073,26 @@ export class AIChatManager implements ChatViewHost {
|
||||
// `displayMessages` are redacted, so a draft built from those would open the form on
|
||||
// the marker rather than on the bytes the model proposed.
|
||||
const entry = this.#runFormEntry(toolId, form)
|
||||
return new Promise((resolve) => {
|
||||
const answer = new Promise<RunFormAnswer | undefined>((resolve) => {
|
||||
this.#runForms.set(toolId, { ...entry, resolve })
|
||||
})
|
||||
// A tab already on this item's deployed page is where the reader would be sent anyway,
|
||||
// so the call goes there without waiting to be asked: declining a proposal and having
|
||||
// the agent propose another leaves that page open, and the second proposal belongs on
|
||||
// it. Only a tab already there — opening one for every proposed run would take the
|
||||
// panel away from whatever the reader is doing. After the resolve above is installed,
|
||||
// since the page reads the call back through `isRunFormPending`.
|
||||
if (form.kind === 'run' && form.path && !form.conversational) {
|
||||
this.openRunOnDeployedPage?.({
|
||||
toolCallId: toolId,
|
||||
kind: form.runnableKind === 'flow' ? 'flow' : 'script',
|
||||
path: form.path,
|
||||
summary: form.summary || form.path,
|
||||
carries: true,
|
||||
onlyIfAlreadyOpen: true
|
||||
})
|
||||
}
|
||||
return answer
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -223,14 +223,31 @@
|
||||
const autoTab = $derived(hasOutcome ? 'outcome' : ran ? 'logs' : 'input')
|
||||
const activeTab = $derived(steered && tabs.some((t) => t.value === steered) ? steered : autoTab)
|
||||
|
||||
// The panel mounts the chat's own form on this call, so the card must not mount a second
|
||||
// one: two views binding the one draft would each reorder the schema SchemaForm edits in
|
||||
// place. Only the form is exclusive. A run shows in both, and collapsing the row or
|
||||
// closing the tab is the user's own way out of seeing it twice.
|
||||
const formInPreview = $derived(
|
||||
pending && (aiChatManager.isRunFormTabOpen?.(message.tool_call_id) ?? false)
|
||||
)
|
||||
// A deployed page holding the call builds its own form from a copy of the proposal, so it
|
||||
// shares no draft and this card keeps a working one. The row folds instead, and expanding
|
||||
// it is how the reader runs from the transcript without going to the page.
|
||||
const carriedByPage = $derived(
|
||||
pending && !formInPreview && (aiChatManager.isRunFormInPreview?.(message.tool_call_id) ?? false)
|
||||
)
|
||||
|
||||
// Keyed by call id: a bare flag would carry one card's collapse onto the next message
|
||||
// reusing this instance. Open by default, since the run is what was asked for —
|
||||
// except for an inspection, which is usually a step in the reasoning rather than
|
||||
// the answer, and which pays a fetch for being opened. A run cancelled before it started
|
||||
// is closed too: the row already says Cancelled, and there is no run to show.
|
||||
// is closed too: the row already says Cancelled, and there is no run to show. So is a call
|
||||
// whose page is on screen, which is the copy the reader is looking at.
|
||||
let toggled = $state<{ id: string; open: boolean } | undefined>(undefined)
|
||||
const expanded = $derived(
|
||||
toggled?.id === message.tool_call_id ? toggled.open : !inspected && (ran || !canceled)
|
||||
toggled?.id === message.tool_call_id
|
||||
? toggled.open
|
||||
: !inspected && (ran || !canceled) && !carriedByPage
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
@@ -285,14 +302,6 @@
|
||||
}
|
||||
})
|
||||
|
||||
// The panel mounts the chat's own form on this call, so the card must not mount a second
|
||||
// one: two views binding the one draft would each reorder the schema SchemaForm edits in
|
||||
// place. Only the form is exclusive. A run shows in both, and collapsing the row or
|
||||
// closing the tab is the user's own way out of seeing it twice.
|
||||
const formInPreview = $derived(
|
||||
pending && (aiChatManager.isRunFormInPreview?.(message.tool_call_id) ?? false)
|
||||
)
|
||||
|
||||
// The three moments a run moves the card — a tab arrives, the selection follows it, the body
|
||||
// changes under it — are staggered rather than landing in one frame, which is what makes the
|
||||
// card followable. A tab already on screen when the card mounts never arrived, so a settled
|
||||
@@ -454,12 +463,6 @@
|
||||
|
||||
function openPreview() {
|
||||
const label = runnableName
|
||||
// The panel now shows what this card was showing, so the card folds away rather than
|
||||
// holding a second copy open in the transcript. Through `toggled`, so it reads as an
|
||||
// ordinary collapse the reader can undo. Only when the page is taking the run: a page
|
||||
// opened to look at leaves the form here, and folding it away would hide the only
|
||||
// place this call can still be confirmed.
|
||||
if (carries) toggled = { id: message.tool_call_id, open: false }
|
||||
if (previewTarget === 'form') {
|
||||
aiChatManager.openRunForm?.({ toolCallId: message.tool_call_id, label })
|
||||
return
|
||||
|
||||
@@ -636,7 +636,14 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
}
|
||||
// Show a deployed run on the item's own page. With `carries`, that page answers the call:
|
||||
// the job its Run starts is the one the tool follows.
|
||||
manager.openRunOnDeployedPage = ({ toolCallId, kind, path, summary, carries }) => {
|
||||
manager.openRunOnDeployedPage = ({
|
||||
toolCallId,
|
||||
kind,
|
||||
path,
|
||||
summary,
|
||||
carries,
|
||||
onlyIfAlreadyOpen
|
||||
}) => {
|
||||
// Through the shared adapter: a session target spells a code-based app `raw_app` and a
|
||||
// workspace item spells it `app` with a flag, and one place should know that.
|
||||
const target = previewTargetForSessionTarget(kind, path)
|
||||
@@ -661,6 +668,9 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
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.
|
||||
if (onlyIfAlreadyOpen) return false
|
||||
previewTabs.open(viewTarget)
|
||||
}
|
||||
return true
|
||||
@@ -677,17 +687,23 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
if (carrying) previewTabs.retargetById(carrying.id, runHref)
|
||||
previewTabs.retargetRunForm(toolCallId, runHref)
|
||||
}
|
||||
// Read off the tab list rather than the slot's lifecycle: a tab the user has switched
|
||||
// away from is unmounted but still open, and the card must keep its form hidden until it
|
||||
// is closed. A resolver, like activePreviewResolver: the reader's own $derived subscribes
|
||||
// to `tabs` through it, and the runtime is not inside an effect root to push from.
|
||||
// A page carrying the call counts: its form is this call's, so the card must not mount a
|
||||
// second one. Both read the same fact, so a tab closed or re-pointed hands the form back.
|
||||
// Both read off the tab list rather than the slot's lifecycle: a tab the user has switched
|
||||
// away from is unmounted but still open. Resolvers, like activePreviewResolver: the
|
||||
// reader's own $derived subscribes to `tabs` through them, and the runtime is not inside
|
||||
// an effect root to push from. So a tab closed or re-pointed hands the card back whatever
|
||||
// it had given up.
|
||||
// Either surface — the chat's own form in a tab, or the item's page carrying the call.
|
||||
manager.isRunFormInPreview = (toolCallId) =>
|
||||
previewTabs.tabs.some(
|
||||
(t) =>
|
||||
parseRunFormRoute(t.url)?.toolCallId === toolCallId || pendingRunIdOf(t.url) === toolCallId
|
||||
)
|
||||
// The chat's own form alone. The card's form is exclusive with this one and no other:
|
||||
// both bind the one draft, and SchemaForm reorders in place the schema it is handed, so a
|
||||
// second mount reorders it again. A page builds its own form from a copy of the proposal
|
||||
// and shares nothing, so the card keeps its form while a page carries the call.
|
||||
manager.isRunFormTabOpen = (toolCallId) =>
|
||||
previewTabs.tabs.some((t) => parseRunFormRoute(t.url)?.toolCallId === toolCallId)
|
||||
|
||||
manager.openArtifact = (id, name, version) => {
|
||||
previewTabs.open({ type: 'artifact', id, name, version })
|
||||
|
||||
Reference in New Issue
Block a user