fix: keep the refused form on the side it is showing

Closing the JSON editor when the form starts refusing writes mounted
`SchemaForm` in its place, and `PasswordArgInput` mints a variable from the
literal it opens on without consulting `disabled` — so a reader who typed a
plaintext secret into JSON view had it stored by the act of entering plan mode,
with no run and no Run press.

The form no longer swaps sides while it refuses: `JsonInputs` takes `readOnly`
and the editor stays mounted and uneditable. The JSON toggle is already hidden
in that state, which is what keeps the fields from mounting from the other
direction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-13 02:39:42 -07:00
co-authored by Claude Opus 5
parent 27cb8e7ae6
commit ef870a52e4
4 changed files with 25 additions and 7 deletions
@@ -13,13 +13,18 @@
* payload nobody has typed into tracks the schema instead of going stale. The first edit
* hands the buffer to the user and later changes stop overwriting it. */
initialCode?: string
/** Show the payload without taking edits. For a caller that must keep this editor
* mounted while refusing writes: swapping it for the fields instead would mount the
* widgets, and a `password` field mints a variable from whatever literal it opens on. */
readOnly?: boolean
}
let {
updateOnBlur = true,
placeholder = 'Write a JSON payload. The input schema will be inferred.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}',
selected = false,
initialCode = ''
initialCode = '',
readOnly = false
}: Props = $props()
let pendingJson = $state(untrack(() => initialCode))
@@ -130,6 +135,7 @@
bind:code={pendingJson}
lang="json"
class="h-full json-inputs-editor"
{readOnly}
{placeholder}
/>
</div>
+8 -4
View File
@@ -135,10 +135,13 @@
}
| undefined
/** Set to refuse every write this form can make, and to say so when a run is
* attempted. No edits, no dynamic-select helper, no JSON, and no run — `run()` mints
* the `password` arguments it is about to send, so refusing in `runAction` is already
* too late. For a form standing in for a chat card under plan mode, which promised
* none of it. */
* attempted: the fields are disabled, the dynamic-select helper is not wired, the JSON
* editor is read-only, and `run()` refuses before it mints the `password` arguments it
* would send — so a caller cannot gate this in `runAction`, which comes after that
* mint. For a form standing in for a chat card under plan mode, which promised none of
* it. The caller must also leave this form on the side it is showing: swapping fields
* for JSON or back mounts a `password` widget, which mints from the literal it opens
* on regardless of being disabled. */
runRefusal?: string
runAction: (
scheduledForStr: string | undefined,
@@ -349,6 +352,7 @@
>
<JsonInputs
bind:this={jsonEditor}
readOnly={argsReadonly}
on:select={(e) => {
// The other way into the arguments, and the one disabling the fields misses.
if (argsReadonly) return
@@ -865,6 +865,10 @@
{inputSelected}
/>
<!-- Not offered while the form refuses writes: each side of this
toggle mounts what the other unmounts, and a `password` field
mints a variable from the literal it opens on. The form stays on
whichever side it is already showing, read-only. -->
{#if hasSchema && !pendingRun?.planModeActive}
<Toggle
bind:checked={jsonView}
@@ -921,7 +925,7 @@
bind:args
schedulable={!pendingRun}
bind:this={runForm}
jsonView={jsonView && !pendingRun?.planModeActive}
{jsonView}
actions={promptForAi ? undefined : aiAssistant}
/>
</div>
@@ -1059,6 +1059,10 @@
}}
{inputSelected}
/>
<!-- Not offered while the form refuses writes: each side of this toggle
mounts what the other unmounts, and a `password` field mints a
variable from the literal it opens on. The form stays on whichever
side it is already showing, read-only. -->
{#if hasSchema && !pendingRun?.planModeActive}
<Toggle
bind:checked={jsonView}
@@ -1135,7 +1139,7 @@
bind:args
schedulable={!pendingRun}
bind:this={runForm}
jsonView={jsonView && !pendingRun?.planModeActive}
{jsonView}
actions={promptForAi ? undefined : aiAssistant}
/>
</div>