fix: simplify the run page AI card to a prompt and a chat button (#11561)

* fix: drop the add a prompt for AI button from the run page

* fix: open the run page in the session preview from a plain chat button

* fix: rename additional prompt for AI to AI prompt

* fix: put the run page chat button beside advanced when no AI prompt
This commit is contained in:
Ruben Fiszel
2026-10-06 14:18:13 +02:00
committed by GitHub
parent 4fd4f76d0d
commit 8c74f75de3
7 changed files with 127 additions and 120 deletions
+3 -2
View File
@@ -152,8 +152,8 @@
* form is embedded in a page that is not the runnable's own — an AI session preview
* tab — since there the fragment would land on an unrelated URL. */
syncArgsToUrl?: boolean
/** Controls beside the Run button of a form that cannot schedule, in the row a
* schedulable one gives its Advanced options. */
/** Controls beside the Run button: left of Advanced on a schedulable form, in
* Advanced's place on one that cannot schedule. */
actions?: import('svelte').Snippet
}
@@ -405,6 +405,7 @@
{/snippet}
</Popover>
</div>
{@render actions?.()}
</div>
{@render unparseableError()}
{#if overrideTag}
@@ -8,10 +8,13 @@
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
import { copilotInfo } from '$lib/aiStore'
import { logFeatureUsage } from '$lib/utils/featureUsage'
import { pageHref } from '$lib/components/sessions/previewPaths'
interface Props {
/** Unset for a viewer who cannot edit the item: the card then offers no way to the editor. */
onEditInstructions: (() => void) | undefined
/** Empty renders the AI button alone, sized for the run form's action row, which
* is where the host places it; a prompt makes this a card of its own. */
instructions: string
runnableType: 'script' | 'flow'
path: string | undefined
@@ -37,72 +40,52 @@
aiChatManager.askAi(`Analyze the ${runnableType} form on this page and fill the inputs for me`)
}
// A session cannot reach this page's form (the preview is a separate editor,
// and form filling drives the DOM through NAVIGATOR mode), so the hand-off
// asks it to run the item instead. Naming the DEPLOYED version matters: the
// test_run_* tools prefer drafts, which is not what this page runs.
// The session's preview opens on this same deployed page (`/get/`), not the item's
// editor: someone on a run page came to run the item, not to change it.
const sessionSource = $derived(
path
? {
target: { kind: runnableType, path } as const,
page: () => pageHref(`/${runnableType}s/get/${path}`),
workspaceId: $operatingWorkspace ?? undefined,
beforeOpen: logAsked,
seedPrompt:
`Run the deployed ${runnableType} \`${path}\` for me. Pick sensible inputs, ` +
`tell me what you chose, then run it.` +
(instructions ? `\n\nHow to choose the inputs:\n${instructions}` : '')
beforeOpen: logAsked
}
: undefined
)
</script>
{#if !$copilotInfo.workspaceDisabled}
{#snippet aiButton(unifiedSize: 'sm' | 'md')}
<!-- The fallback is a plain Button rather than AskAiButton, whose own session branch
would fire here too and open an empty session. -->
<OpenInSessionButton source={sessionSource} btnProps={{ unifiedSize }}>
{#snippet fallback()}
<Button
{unifiedSize}
startIcon={{ icon: WandSparkles }}
btnClasses={AIBtnClasses('default')}
onclick={fillFormWithAI}
>
Fill with AI
</Button>
{/snippet}
</OpenInSessionButton>
{/snippet}
{#if $copilotInfo.workspaceDisabled}
<!-- The workspace hid the assistant. -->
{:else if instructions}
<div class="my-2 flex flex-col gap-1">
<div class="flex flex-row gap-2 justify-between items-center">
{#if instructions}
<Button
variant="subtle"
unifiedSize="sm"
startIcon={{ icon: expanded ? ChevronDown : ChevronRight }}
onclick={() => (expanded = !expanded)}
>
Additional prompt for AI
</Button>
{:else if onEditInstructions}
<Button
variant="subtle"
unifiedSize="sm"
startIcon={{ icon: Pencil }}
onclick={onEditInstructions}
>
Add a prompt for AI
</Button>
{:else}
<span></span>
{/if}
<!-- Each button names its own action because the two branches do different things:
the hand-off runs the item, the legacy path fills the form. A plain Button rather
than AskAiButton, whose own session branch would fire here too and open an empty
session. -->
<OpenInSessionButton
source={sessionSource}
label="Run in AI session"
tooltip="Open an AI session that picks inputs and runs this"
btnProps={{ iconOnly: false, unifiedSize: 'sm', startIcon: { icon: WandSparkles } }}
<Button
variant="subtle"
unifiedSize="sm"
startIcon={{ icon: expanded ? ChevronDown : ChevronRight }}
onclick={() => (expanded = !expanded)}
>
{#snippet fallback()}
<Button
unifiedSize="sm"
startIcon={{ icon: WandSparkles }}
btnClasses={AIBtnClasses('default')}
onclick={fillFormWithAI}
>
Fill with AI
</Button>
{/snippet}
</OpenInSessionButton>
AI prompt
</Button>
{@render aiButton('sm')}
</div>
{#if instructions && expanded}
{#if expanded}
<div
transition:slide={{ duration: 120 }}
class="flex flex-row gap-2 items-start justify-between p-2 bg-surface-secondary rounded-md"
@@ -120,4 +103,6 @@
</div>
{/if}
</div>
{:else}
{@render aiButton('md')}
{/if}
@@ -13,7 +13,7 @@
{#if !$copilotInfo.workspaceDisabled}
<Label
label="Additional prompt for AI"
label="AI prompt"
tooltip="An AI choosing this {type}'s inputs already reads its description and each field's description. Anything written here is given to it as extra guidance: mention specific fields and how they interact."
>
<TextInput
@@ -139,6 +139,7 @@
// lookup does. Assigned once, that leaves the page permanently read-only — `canWrite`
// refuses an unknown user — with nothing to recompute it.
const can_write = $derived(!!flow && canWrite(flow.path, flow.extra_perms!, actingUser))
const promptForAi = $derived((flow?.schema?.prompt_for_ai as string | undefined) ?? '')
let shareModal: ShareModal | undefined = $state()
let scheduledForStr: string | undefined = $state(undefined)
@@ -852,15 +853,20 @@
</div>
{/if}
{#if Object.keys(flow.schema?.properties ?? {}).length > 0}
<AIFormAssistant
instructions={(flow.schema?.prompt_for_ai as string | undefined) ?? ''}
onEditInstructions={can_write && canAuthorFlow
? () => onNavigate(`/flows/edit/${flow?.path}`)
: undefined}
runnableType="flow"
path={flow?.path}
/>
{#snippet aiAssistant()}
{#if Object.keys(flow?.schema?.properties ?? {}).length > 0}
<AIFormAssistant
instructions={promptForAi}
onEditInstructions={can_write && canAuthorFlow
? () => onNavigate(`/flows/edit/${flow?.path}`)
: undefined}
runnableType="flow"
path={flow?.path}
/>
{/if}
{/snippet}
{#if promptForAi}
{@render aiAssistant()}
{/if}
<RunForm
@@ -879,6 +885,7 @@
bind:args
bind:this={runForm}
{jsonView}
actions={promptForAi ? undefined : aiAssistant}
/>
</div>
@@ -166,6 +166,7 @@
// asynchronously, and a value read after `getScriptByHash` resolves can land before that
// lookup does. Assigned once, that leaves the page permanently read-only — `canWrite`
// refuses an unknown user — with nothing to recompute it. A hub script is never writable.
const promptForAi = $derived((script?.schema?.prompt_for_ai as string | undefined) ?? '')
const can_write = $derived(
!isHubScript &&
!!script &&
@@ -1073,15 +1074,20 @@
</div>
{/if}
{#if Object.keys(script?.schema?.properties ?? {}).length > 0}
<AIFormAssistant
instructions={(script?.schema?.prompt_for_ai as string | undefined) ?? ''}
onEditInstructions={can_write && !actingUser?.operator
? () => onNavigate(`/scripts/edit/${script?.path}?metadata_open=true`)
: undefined}
runnableType="script"
path={script?.path}
/>
{#snippet aiAssistant()}
{#if Object.keys(script?.schema?.properties ?? {}).length > 0}
<AIFormAssistant
instructions={promptForAi}
onEditInstructions={can_write && !actingUser?.operator
? () => onNavigate(`/scripts/edit/${script?.path}?metadata_open=true`)
: undefined}
runnableType="script"
path={script?.path}
/>
{/if}
{/snippet}
{#if promptForAi}
{@render aiAssistant()}
{/if}
<RunForm
@@ -1101,6 +1107,7 @@
schedulable={true}
bind:this={runForm}
{jsonView}
actions={promptForAi ? undefined : aiAssistant}
/>
</div>
@@ -93,7 +93,7 @@
let activeAdvancedOptions = $derived([
{
name: 'Additional prompt for AI',
name: 'AI prompt',
active: !!flowStore.val.schema?.prompt_for_ai
},
{
@@ -7,7 +7,7 @@
* is caught here and turned into a move inside the preview panel, so nothing takes
* the browser out of the session.
*/
import { untrack } from 'svelte'
import { setContext, untrack } from 'svelte'
import { base } from '$lib/base'
import ScriptDetail from '$lib/components/details/ScriptDetail.svelte'
import FlowDetail from '$lib/components/details/FlowDetail.svelte'
@@ -48,6 +48,13 @@
active?: boolean
} = $props()
// Mounted in the preview panel, outside the chat's own subtree, as PageItemEditorView is:
// the detail pages' AI entry points would otherwise offer a session from inside one.
setContext(
'aiChatManager',
untrack(() => runtime.manager)
)
// This reads the deployed version over the API rather than from the editor cell the
// chat mutates, so it is the one preview kind that does not self-sync (the invariant
// `previewReload.ts` states for editors). A deploy from the editor topbar beside it
@@ -179,50 +186,50 @@
<!-- Remount to refetch: both detail components load on mount, so a deploy lands here
rather than as a refresh path of their own. -->
{#key loadKey}
{@const mountedKey = loadKey}
{@const setLoadState = (state: 'loaded' | 'not_found') => onLoadState(mountedKey, state)}
{#if notDeployed}
<!-- This side shows the DEPLOYED item, and there isn't one. Without this the detail
{#key loadKey}
{@const mountedKey = loadKey}
{@const setLoadState = (state: 'loaded' | 'not_found') => onLoadState(mountedKey, state)}
{#if notDeployed}
<!-- This side shows the DEPLOYED item, and there isn't one. Without this the detail
page renders nothing and the panel is simply blank, which reads as a failure
rather than as the ordinary state of a draft nobody has deployed yet. -->
<div class="flex-1 flex flex-col items-center justify-center gap-3 p-8 text-center">
<div class="text-sm text-primary">Not deployed yet</div>
<div class="text-xs text-tertiary max-w-sm">
Nothing is deployed at <span class="font-mono">{path}</span>, so there is no deployed
version to view. Deploy it from the editor to see it here.
</div>
<Button unifiedSize="sm" variant="default" startIcon={{ icon: Pen }} on:click={toEditSide}>
Back to editor
</Button>
<div class="flex-1 flex flex-col items-center justify-center gap-3 p-8 text-center">
<div class="text-sm text-primary">Not deployed yet</div>
<div class="text-xs text-tertiary max-w-sm">
Nothing is deployed at <span class="font-mono">{path}</span>, so there is no deployed
version to view. Deploy it from the editor to see it here.
</div>
{:else if kind === 'script'}
<ScriptDetail
hash={path}
workspace={workspaceId}
{searchParams}
{onNavigate}
{active}
embedded
onLoadState={setLoadState}
/>
{:else if kind === 'flow'}
<FlowDetail
{path}
workspace={workspaceId}
{searchParams}
{onNavigate}
{active}
embedded
onLoadState={setLoadState}
/>
{:else}
<InWorkspaceAppViewer
workspace={workspaceId}
{path}
onEdit={() => onNavigate(`/apps_raw/edit/${path}`)}
onLoadState={setLoadState}
syncHashToUrl={false}
/>
{/if}
{/key}
<Button unifiedSize="sm" variant="default" startIcon={{ icon: Pen }} on:click={toEditSide}>
Back to editor
</Button>
</div>
{:else if kind === 'script'}
<ScriptDetail
hash={path}
workspace={workspaceId}
{searchParams}
{onNavigate}
{active}
embedded
onLoadState={setLoadState}
/>
{:else if kind === 'flow'}
<FlowDetail
{path}
workspace={workspaceId}
{searchParams}
{onNavigate}
{active}
embedded
onLoadState={setLoadState}
/>
{:else}
<InWorkspaceAppViewer
workspace={workspaceId}
{path}
onEdit={() => onNavigate(`/apps_raw/edit/${path}`)}
onLoadState={setLoadState}
syncHashToUrl={false}
/>
{/if}
{/key}