Files
windmill/frontend/src/lib/components/JobArgs.svelte
T
AlexRV12andClaude Opus 5 a6abf2c8a7 feat: run and test scripts from the AI chat through an argument form (#11001)
* fix: disable a dynamic input when its schema field is disabled

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* refactor: extract the run form's argument hygiene into job_args

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* feat: give Tabs an opt-in sliding selection indicator

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* refactor: share the chat's scroll-fade measurement

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* feat: give the chat a run-form contract and incremental job output

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* feat: run and test a script from the chat through an argument form

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* feat: carry a chat run's card and job across saves and reloads

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* feat: render a chat run as a tool call row with its form, logs and result

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* feat: open a pending run form in the sessions preview pane

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* test: benchmark running a deployed script from the chat

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh

* feat: offer a test run's dynamic options from the draft it previews

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: say that a test run's dynselect helper executes on form display

* fix: send a schema default the model omitted when yolo skips the form

* fix: infer a test run's schema when the stored one declares no properties

* docs: tighten the note on the form's mount-time helper job

* fix: apply a nested schema default the bypass posture counts as answered

* fix: apply a declared default to a null value and an optional nested field

* fix: check required fields inside a supplied optional object before bypassing

* fix: read required args as own properties before bypassing the form

* fix: stop the turn from the run form's action row in the preview panel

* refactor: drop the run-form prediction and share its secret minting

* refactor: prefill a proposed secret instead of emptying the field

* docs: correct the comments the run-form prediction left behind

* fix: keep a proposed secret out of the chat's stored messages

* docs: say what a literal secret argument now does

* test: restore the copilotInfo export the aiStore mock omits

* docs: cut the run form's helper-script note to its constraints

* refactor: settle a run form from one entry and fetch a job's logs once

* fix: separate colliding secret paths, gate plan mode, keep polled logs

* fix: mint before the form opens, skip empty fields, show what ran

* revert: mint a run form's secrets at submit, not before it opens

* fix: settle a cancelled run card on the form's arguments, not the proposal

* fix: settle a stopped run form like a cancelled one, and keep an empty secret empty

* fix: snapshot a run's arguments before minting its secrets

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 12:31:58 +00:00

259 lines
7.8 KiB
Svelte

<script lang="ts">
import { ChevronRightSquare, ClipboardCopy, Download, Expand } from 'lucide-svelte'
import ArgInfo from './ArgInfo.svelte'
import { Badge, Button, Drawer, DrawerContent, Skeleton } from './common'
import { Highlight } from 'svelte-highlight'
import { copyToClipboard } from '$lib/utils'
import { json } from 'svelte-highlight/languages'
import DataTable from './table/DataTable.svelte'
import Cell from './table/Cell.svelte'
import Head from './table/Head.svelte'
import Row from './table/Row.svelte'
import HighlightTheme from './HighlightTheme.svelte'
import { deepEqual } from 'fast-equals'
import { isWindmillTooBigObject } from './job_args'
import { downloadViaClient, shouldDownloadViaClient } from '$lib/utils/downloadFile'
import { appendViewToken } from '$lib/viewToken'
interface Props {
id?: string | undefined
args: any
argLabel?: string | undefined
workspace?: string | undefined
/** Drop the header's expand-into-a-drawer button, for a caller that already offers a
* way to open the run in full. */
disableExpand?: boolean
}
let {
id = undefined,
args,
argLabel = undefined,
workspace = undefined,
disableExpand = false
}: Props = $props()
// Internal flag injected by "test this step" runs to suppress the asset
// dispatcher. Not a real input: shown as a badge instead of a table row,
// but kept in the expanded JSON drawer and download.
const SKIP_ASSET_DISPATCH_ARG = '_wmill_skip_asset_dispatch'
// Internal map (path -> content digest) injected by local-dev previews so
// relative imports resolve from not-yet-deployed local content. Same
// treatment: badge, not a noisy table row.
const TEMP_SCRIPT_REFS_ARG = '_TEMP_SCRIPT_REFS'
const INTERNAL_ARGS = [SKIP_ASSET_DISPATCH_ARG, TEMP_SCRIPT_REFS_ARG]
let skippedAssetDispatch = $derived(
args != undefined && typeof args === 'object' && args[SKIP_ASSET_DISPATCH_ARG] === true
)
let hasTempScriptRefs = $derived(
args != undefined && typeof args === 'object' && args[TEMP_SCRIPT_REFS_ARG] != undefined
)
let displayArgs = $derived(
args != undefined && typeof args === 'object'
? Object.fromEntries(Object.entries(args).filter(([k]) => !INTERNAL_ARGS.includes(k)))
: args
)
let jsonViewer: Drawer | undefined = $state()
let runLocally: Drawer | undefined = $state()
let jsonStr = $state('')
const argsDownloadName = 'windmill-args.json'
let argsApiPath = $derived(
id && workspace ? appendViewToken(`/w/${workspace}/jobs_u/get_args/${id}`) : undefined
)
let argsDataHref = $derived(`data:text/json;charset=utf-8,${encodeURIComponent(jsonStr)}`)
function pythonCode() {
return `
if __name__ == "__main__":
${Object.entries(displayArgs)
.map(([arg, value]) => {
return ` ${arg} = ${JSON.stringify(value)}`
})
.join('\n')}
main(${Object.keys(displayArgs)
.map((x) => `${x} = ${x}`)
.join(', ')})
`
}
function typescriptCode() {
return `
if (import.meta.main) {
${Object.entries(displayArgs)
.map(([arg, value]) => {
return ` let ${arg} = ${JSON.stringify(value)}`
})
.join('\n')}
await main(...)
}
`
}
</script>
{#if args && typeof args === 'object' && deepEqual( Object.keys(args ?? {}), ['reason'] ) && args['reason'] == 'PREPROCESSOR_ARGS_ARE_DISCARDED'}
Preprocessor args are discarded
{:else if id && workspace && args && typeof args === 'object' && deepEqual( Object.keys(args ?? {}), ['reason'] ) && args['reason'] == 'WINDMILL_TOO_BIG'}
The args are too big in size to be able to fetch alongside job. Please {#if shouldDownloadViaClient()}<button
class="text-blue-500 hover:underline"
onclick={() => downloadViaClient(argsApiPath!, argsDownloadName)}
>download the JSON file to view them</button
>{:else}<a href="/api{argsApiPath}" target="_blank">download the JSON file to view them</a
>{/if}.
{:else}
<div class="relative">
<DataTable size="sm" containerClass="bg-surface-tertiary">
<Head>
<tr class="w-full">
<Cell head first>
{argLabel ?? 'Input'}
{#if skippedAssetDispatch}
<Badge
color="gray"
verySmall
title="This run was started with {SKIP_ASSET_DISPATCH_ARG} and did not auto-trigger downstream asset consumers"
>
asset dispatch skipped
</Badge>
{/if}
{#if hasTempScriptRefs}
<Badge
color="gray"
verySmall
title="This preview carried {TEMP_SCRIPT_REFS_ARG} so relative imports resolve from local (not-yet-deployed) script content"
>
local import refs
</Badge>
{/if}
</Cell>
<Cell head last>Value</Cell>
</tr>
{#snippet headerAction()}
{#if !disableExpand}
<div class="center-center -m-1">
<Button
unifiedSize="md"
variant="subtle"
onClick={() => {
jsonStr = JSON.stringify(args, null, 4)
jsonViewer?.openDrawer()
}}
iconOnly
startIcon={{ icon: Expand }}
></Button>
</div>
{/if}
{/snippet}
</Head>
<tbody class="divide-y w-full">
{#if displayArgs && typeof displayArgs === 'object' && Object.keys(displayArgs ?? {}).length > 0}
{#each Object.entries(displayArgs ?? {}).sort( (a, b) => a?.[0]?.localeCompare(b?.[0]) ) as [arg, value]}
<Row>
<Cell first>{arg}</Cell>
<Cell><ArgInfo {value} /></Cell>
</Row>
{/each}
{:else if displayArgs && typeof displayArgs !== 'object'}
<Row><Cell>Argument is not an object (type: {typeof displayArgs})</Cell></Row>
{:else if args}
<Row><Cell>No arguments</Cell></Row>
{:else}
<Row>
<Cell first>
<Skeleton layout={[[1], 0.5, [1]]} />
</Cell>
<Cell last>
<Skeleton layout={[[1], 0.5, [1]]} />
</Cell>
</Row>
{/if}
</tbody>
</DataTable>
</div>
{/if}
<HighlightTheme />
<Drawer bind:this={jsonViewer} size="900px">
<DrawerContent title="Expanded Args" on:close={jsonViewer.closeDrawer}>
{#snippet actions()}
{#if argsApiPath && shouldDownloadViaClient()}
<Button
on:click={() => downloadViaClient(argsApiPath!, argsDownloadName)}
startIcon={{ icon: Download }}
size="xs"
color="light"
>
Download
</Button>
{:else}
<Button
download={argsDownloadName}
href={argsApiPath ? `/api${argsApiPath}` : argsDataHref}
startIcon={{ icon: Download }}
size="xs"
color="light"
>
Download
</Button>
{/if}
<Button
on:click={() => runLocally?.openDrawer()}
color="light"
size="xs"
startIcon={{ icon: ChevronRightSquare }}
>
Use in a local script
</Button>
<Button
on:click={() => copyToClipboard(jsonStr)}
color="light"
size="xs"
startIcon={{ icon: ClipboardCopy }}
>
Copy to clipboard
</Button>
{/snippet}
{#if jsonStr.length > 100000 || (id && workspace && args && isWindmillTooBigObject(args))}
<div class="text-sm mb-2 text-primary">
{#if argsApiPath && shouldDownloadViaClient()}
<button
class="underline"
onclick={() => downloadViaClient(argsApiPath!, argsDownloadName)}
>
JSON is too large to be displayed in full.
</button>
{:else}
<a download={argsDownloadName} href={argsApiPath ? `/api${argsApiPath}` : argsDataHref}>
JSON is too large to be displayed in full.
</a>
{/if}
</div>
{:else}
<Highlight language={json} code={jsonStr.replace(/\\n/g, '\n')} />
{/if}
</DrawerContent>
</Drawer>
<Drawer bind:this={runLocally} size="900px">
<DrawerContent title="Run locally" on:close={runLocally.closeDrawer}>
<h3 class="mb-2">Envs</h3>
If using the wmill client in your code, set the following env variables:
<pre
><code
>BASE_URL="{window.location.origin}"
WM_TOKEN="{'<TOKEN>'}"</code
></pre
>
<h3 class="mt-8">TypeScript</h3>
<pre><code>{typescriptCode()}</code></pre>
<h3 class="mt-8">Python</h3>
<pre><code>{pythonCode()}</code></pre>
</DrawerContent>
</Drawer>