mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
fix(sessions): apply a run proposal once, not on every remount
The detail page remounts under its viewer whenever the item is deployed, refreshed or pinned to a version, and came back with its own latch reset — so the agent's proposal was pushed in again, over whatever the reader had typed since, bringing back a note they had dismissed. The proposal was made once. The viewer now holds it as pending until the page reports applying it, and says separately whether to credit the agent: the note stands until dismissed, the values land once. Also from the CI round's nits: - a pinned viewer the handover re-points is brought to the front, since the call is about to say the page is open - offerMessage's doc described inputs it no longer carries; applyInputs owns them, deliberately unconditional on the message landing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
465c15aa6b
commit
904abda69b
@@ -104,6 +104,8 @@
|
||||
active = true,
|
||||
embedded = false,
|
||||
seededRun,
|
||||
seededByAgent = false,
|
||||
onSeedApplied,
|
||||
onClearSeededRun,
|
||||
onLoadState
|
||||
}: {
|
||||
@@ -125,7 +127,12 @@
|
||||
* note above them. `seq` identifies the request, so a second one re-seeds a page that
|
||||
* is already open. */
|
||||
seededRun?: { args: Record<string, any>; seq: number }
|
||||
/** The reader dismissed that note. The values stay in the fields. */
|
||||
/** Whether to say the agent filled them. Outlives `seededRun`, since the note stands
|
||||
* until the reader dismisses it. */
|
||||
seededByAgent?: boolean
|
||||
/** Applied, so the host stops offering them. */
|
||||
onSeedApplied?: () => void
|
||||
/** The reader dismissed the note. The values stay in the fields. */
|
||||
onClearSeededRun?: () => void
|
||||
/** How the load ended, for a host that renders its own state around this page. */
|
||||
onLoadState?: (state: 'loaded' | 'not_found') => void
|
||||
@@ -402,9 +409,11 @@
|
||||
// would put words in the box long after they were proposed. The card still shows
|
||||
// what was asked for.
|
||||
if (typeof message === 'string' && message) flowChat.offerMessage(message)
|
||||
onSeedApplied?.()
|
||||
} else if (runForm) {
|
||||
seededSeq = seededRun.seq
|
||||
runForm.setArgs(seededRun.args)
|
||||
onSeedApplied?.()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -920,7 +929,7 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if seededRun}
|
||||
{#if seededByAgent}
|
||||
<InputSelectedBadge inputSelected="agent" onReject={() => onClearSeededRun?.()} />
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -127,6 +127,8 @@
|
||||
active = true,
|
||||
embedded = false,
|
||||
seededRun,
|
||||
seededByAgent = false,
|
||||
onSeedApplied,
|
||||
onClearSeededRun,
|
||||
onLoadState
|
||||
}: {
|
||||
@@ -143,12 +145,19 @@
|
||||
/** Rendered inside a page that is not the script's own (an AI session preview tab), whose
|
||||
* URL this must leave alone and which a cross-workspace link must not navigate away. */
|
||||
embedded?: boolean
|
||||
/** Arguments a chat tool filled this form with when it opened the page. Nothing waits
|
||||
* on them: the page runs as it always does, and these only seed the fields and put a
|
||||
* note above them. `seq` identifies the request, so a second one re-seeds a page that
|
||||
* is already open. */
|
||||
/** Arguments a chat tool proposed when it opened the page, for as long as they have
|
||||
* still to be applied. Nothing waits on them: the page runs as it always does, and
|
||||
* these only fill the fields. `seq` identifies the request, so a second one re-seeds a
|
||||
* page already open. Gone once applied — this page remounts under its host on a
|
||||
* deploy, a refresh or a version pin, and applying again would overwrite what the
|
||||
* reader has typed since. */
|
||||
seededRun?: { args: Record<string, any>; seq: number }
|
||||
/** The reader dismissed that note. The values stay in the fields. */
|
||||
/** Whether to say the agent filled them. Outlives `seededRun`, since the note stands
|
||||
* until the reader dismisses it. */
|
||||
seededByAgent?: boolean
|
||||
/** Applied, so the host stops offering them. */
|
||||
onSeedApplied?: () => void
|
||||
/** The reader dismissed the note. The values stay in the fields. */
|
||||
onClearSeededRun?: () => void
|
||||
/** How the load ended, for a host that renders its own state around this page.
|
||||
* Providing it also suppresses the "could not load" toast: a 404 here is the normal
|
||||
@@ -432,6 +441,7 @@
|
||||
if (seededSeq === seededRun.seq) return
|
||||
seededSeq = seededRun.seq
|
||||
runForm.setArgs(seededRun.args)
|
||||
onSeedApplied?.()
|
||||
})
|
||||
|
||||
// Read once on purpose: these args seed the form, so tracking the fragment would
|
||||
@@ -1105,7 +1115,7 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if seededRun}
|
||||
{#if seededByAgent}
|
||||
<InputSelectedBadge inputSelected="agent" onReject={() => onClearSeededRun?.()} />
|
||||
{/if}
|
||||
<RunForm
|
||||
|
||||
@@ -178,14 +178,10 @@
|
||||
const shownKey = $derived(poolState?.shownKey)
|
||||
|
||||
/** Put a message in the composer of the conversation on screen, for the reader to edit and
|
||||
* send, and set the flow's other inputs to go with it. Returns false when that composer
|
||||
* already holds a draft — it is never overwritten, and the caller leaves the message where
|
||||
* the reader can still see it.
|
||||
*
|
||||
* `inputs` are the run's non-message arguments: a chat flow's schema can declare more than
|
||||
* `user_message`, and a message sent without them would run on saved values or schema
|
||||
* defaults — a different run from the one being offered. Applied only alongside a message
|
||||
* the composer took, so a declined offer leaves the reader's settings alone. */
|
||||
* send. Returns false when that composer already holds a draft — it is never overwritten,
|
||||
* and the caller drops the message rather than holding it. The run's other arguments go
|
||||
* through {@link applyInputs}, which is deliberately not conditional on this: they belong
|
||||
* to the proposal whether or not its message could land. */
|
||||
export function offerMessage(text: string): boolean {
|
||||
const panel = shownKey ? pool?.get(shownKey) : undefined
|
||||
return panel?.host.offerMessage(text) ?? false
|
||||
|
||||
@@ -69,22 +69,40 @@
|
||||
// Arguments a chat tool filled this item's run form with when it opened the page. Nothing
|
||||
// is waiting on them: the reader runs the page as they would with no chat open, and the
|
||||
// note above the form only says where the values came from.
|
||||
// Taken from the runtime once and held here: the request belongs to the turn that made it,
|
||||
// so leaving it in the map would re-apply a stale proposal — and reannounce it — to every
|
||||
// later open of this item's deployed page.
|
||||
// Carries the item it was filed under: this tab re-points to another item in place rather
|
||||
// than remounting, so a seed held without its key would be pushed into the next item's
|
||||
// form — replacing its own defaults — under a note crediting the agent for them.
|
||||
//
|
||||
// Taken from the runtime once and held here, because the request belongs to the turn that
|
||||
// made it: left in the map it would re-seed every later open of this item's page. The item
|
||||
// it was filed under rides along, since this tab re-points to another item in place rather
|
||||
// than remounting — a seed held without its key would land in the next item's form,
|
||||
// replacing its own defaults, under a note crediting the agent for them.
|
||||
//
|
||||
// `pending` is what the page has still to apply, and the detail page clears it by reporting
|
||||
// that it has. The two are separate because the detail page remounts under this one (a
|
||||
// deploy, a refresh, a version pin) and comes back with its own latch reset: without that,
|
||||
// each remount would push the proposal in again, over whatever the reader has since typed,
|
||||
// and bring back a note they had dismissed. The agent proposed once.
|
||||
let held = $state<
|
||||
{ kind: SessionTargetKind; path: string; args: Record<string, any>; seq: number } | undefined
|
||||
| {
|
||||
kind: SessionTargetKind
|
||||
path: string
|
||||
args: Record<string, any>
|
||||
seq: number
|
||||
pending: boolean
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
const seededRun = $derived(held?.kind === kind && held.path === path ? held : undefined)
|
||||
const forThisItem = $derived(held?.kind === kind && held.path === path ? held : undefined)
|
||||
const seededRun = $derived(
|
||||
forThisItem?.pending ? { args: forThisItem.args, seq: forThisItem.seq } : undefined
|
||||
)
|
||||
/** The note stands until the reader dismisses it, which outlives the one application. */
|
||||
const seededByAgent = $derived(!!forThisItem)
|
||||
$effect(() => {
|
||||
const pending = runtime.seededRunArgsFor(kind, path)
|
||||
if (!pending) return
|
||||
const incoming = runtime.seededRunArgsFor(kind, path)
|
||||
if (!incoming) return
|
||||
const [k, p] = [kind, path]
|
||||
untrack(() => {
|
||||
held = { kind: k, path: p, ...pending }
|
||||
held = { kind: k, path: p, ...incoming, pending: true }
|
||||
runtime.clearSeededRunArgs(k, p)
|
||||
})
|
||||
})
|
||||
@@ -228,6 +246,8 @@
|
||||
{active}
|
||||
embedded
|
||||
{seededRun}
|
||||
{seededByAgent}
|
||||
onSeedApplied={() => held && (held = { ...held, pending: false })}
|
||||
onClearSeededRun={() => (held = undefined)}
|
||||
onLoadState={setLoadState}
|
||||
/>
|
||||
@@ -240,6 +260,8 @@
|
||||
{active}
|
||||
embedded
|
||||
{seededRun}
|
||||
{seededByAgent}
|
||||
onSeedApplied={() => held && (held = { ...held, pending: false })}
|
||||
onClearSeededRun={() => (held = undefined)}
|
||||
onLoadState={setLoadState}
|
||||
/>
|
||||
|
||||
@@ -637,8 +637,15 @@ function createRuntime(session: Session): SessionRuntime {
|
||||
t.url.includes('version=')
|
||||
)
|
||||
})
|
||||
if (pinned) previewTabs.navigate(viewTarget, pinned.id)
|
||||
else previewTabs.open(viewTarget)
|
||||
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.
|
||||
previewTabs.select(pinned.id)
|
||||
} else {
|
||||
previewTabs.open(viewTarget)
|
||||
}
|
||||
return true
|
||||
}
|
||||
manager.closeRunForm = (toolCallId) => previewTabs.closeRunForm(toolCallId)
|
||||
|
||||
Reference in New Issue
Block a user