Files
windmill/frontend/src/lib/components/PasswordArgInput.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

150 lines
5.2 KiB
Svelte

<script lang="ts">
import { VariableService } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { ephemeralSecretPrefix, mintEphemeralSecret } from './secretArgUtils'
import { sendUserToast } from '$lib/toast'
import { Button } from './common'
import Password from './Password.svelte'
import { untrack } from 'svelte'
interface Props {
value?: string | undefined
disabled: boolean
minRows?: number
/** Workspace the ephemeral secret is minted in; defaults to the nav workspace.
* Session editors pass their acting workspace. */
workspace?: string | undefined
}
let { value = $bindable(undefined), disabled, minRows, workspace }: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let path = $state('')
// Workspace the variable at `path` actually lives in; `ws` can move away from it.
let mintedIn = $state<string | undefined>(undefined)
// What the field mints from: an argument already holding a `$var:` ref has nothing to mint.
function plaintextOf(v: unknown): string {
return typeof v === 'string' && v !== '' && !v.startsWith('$var:') ? v : ''
}
let password = $state(plaintextOf(value))
// The argument no longer holds what this field would mint from — a parent can replace the whole
// args object without remounting it (previewing a saved input, say). Minting now would describe a
// secret the argument does not point at, and binding it would discard the replacement.
let argReplaced = $derived(path !== '' && value !== '$var:' + path)
let isGenerating = false
let username = $derived(($userStore?.username ?? $userStore?.email)?.split('@')[0] ?? '')
let userPrefix = $derived(ephemeralSecretPrefix(username))
async function generateValue() {
if (isGenerating || argReplaced) return
isGenerating = true
const mintWs = ws!
const boundBefore = value
try {
let npath = await mintEphemeralSecret(mintWs, username, password)
let nvalue = '$var:' + npath
// The arg can be replaced the same way while the create is in flight. Nothing ever
// referenced the variable just minted, so delete it; it expires on its own if that fails.
if (value !== boundBefore) {
VariableService.deleteVariable({ workspace: mintWs, path: npath }).catch(() => {})
return
}
path = npath
mintedIn = mintWs
console.log('generated', nvalue)
value = nvalue
debouncedUpdate()
} finally {
// Ended without binding: discarded just above, or the create failed after the argument
// moved. The field would otherwise keep showing a secret the argument does not hold, and
// the mint effect tracks `ws` — a workspace move would bind that stale plaintext over the
// replacement. Re-seeding leaves the field describing the argument again.
if (path === '' && value !== boundBefore) {
password = plaintextOf(value)
}
isGenerating = false
}
}
async function updateValue() {
// The first keystroke queues an update before anything is minted: letting it run would 404 and
// retry the mint, binding over an argument that was replaced while the first mint was in flight.
if (path === '') return
const updating = path
try {
await VariableService.updateVariable({
workspace: mintedIn ?? ws!,
path: path,
requestBody: {
value: password
}
})
} catch (e) {
// A re-mint can bind a fresh variable while this update is in flight; recovering then
// would orphan the one it just bound.
if (path !== updating) return
generateValue().catch((e) =>
sendUserToast(`Could not create the secret: ${e?.body ?? e?.message ?? e}`, true)
)
}
}
let timeout: number | undefined = undefined
function debouncedUpdate() {
timeout && clearTimeout(timeout)
timeout = setTimeout(updateValue, 500)
}
$effect(() => {
password && untrack(() => debouncedUpdate())
})
$effect(() => {
ws &&
($userStore?.username || $userStore?.email) &&
path == '' &&
password != '' &&
untrack(() =>
// A failed mint leaves the plaintext bound to nothing and the argument empty. Only a
// further keystroke re-runs this, so say so rather than submitting the job without it.
generateValue().catch((e) =>
sendUserToast(`Could not create the secret: ${e?.body ?? e?.message ?? e}`, true)
)
)
})
// The operating workspace can move after minting (a session forking, say), leaving the
// variable behind where the job will not find it: mint a fresh one in the new workspace.
// Bounded to a live instance: a field mounted onto an existing `$var:` holds neither the
// plaintext nor the workspace it was minted in, so it can only be moved by retyping it.
$effect(() => {
const cur = ws
if (!cur || path === '' || password === '' || mintedIn === cur || argReplaced) return
untrack(() =>
generateValue().catch((e) =>
sendUserToast(`Could not create the secret in ${cur}: ${e?.body ?? e?.message ?? e}`, true)
)
)
})
</script>
{#if value?.startsWith('$var:') && !value.startsWith('$var:' + userPrefix)}
<div class="flex items-center gap-2 text-sm text-primary">
Linked to static variable
<Button
size="xs"
variant="default"
onclick={() => {
value = ''
}}
>
Reset variable link
</Button>
</div>
{:else}
<Password {disabled} {minRows} bind:password />
{/if}