fix: align the run form with the rest of the chat's styling

This commit is contained in:
AlexRV12
2026-09-03 16:31:55 +02:00
parent c802e2690a
commit 49c36fef49
3 changed files with 67 additions and 47 deletions
@@ -115,19 +115,23 @@
class={twMerge('flex flex-col', layout === 'pane' ? 'h-full min-h-0' : 'pt-3')}
data-chat-keyboard-scope="run-args-form"
>
<!-- Only the fields scroll. A script with many arguments would otherwise grow a card
taller than the pane, pushing the Run button and the lines naming what the form
dropped — a secret it opened empty among them — below the fold. `both-edges` reserves
the gutter on both sides, so the fields stay centred rather than drifting left of it. -->
<div class={twMerge('relative flex flex-col', layout === 'pane' ? 'min-h-0 flex-1' : '')}>
<!-- Only the fields scroll, sized by their content rather than filling the host: the actions
follow the last field of a short form, and a long one scrolls under them rather than
pushing the Run button — and the lines naming what the form dropped — below the fold. -->
<div class={twMerge('relative flex flex-col', layout === 'pane' ? 'min-h-0' : '')}>
<!-- `pl-9` is the rail the pane header's title stands on, its padding plus the icon and
the gap beside it, so the tab reads down one edge. The card has no such rail to meet
and reserves the gutter on both sides instead, which keeps its fields centred. -->
<div
use:fadeContainer
onscroll={measureFades}
class={twMerge(
'overflow-y-auto px-3',
layout === 'pane' ? 'min-h-0 flex-1' : 'max-h-[min(28rem,50vh)]'
'overflow-y-auto',
layout === 'pane' ? 'min-h-0 pl-9 pr-3' : 'max-h-[min(28rem,50vh)] px-3'
)}
style="scrollbar-gutter: stable both-edges;"
style={layout === 'pane'
? 'scrollbar-gutter: stable;'
: 'scrollbar-gutter: stable both-edges;'}
>
<div use:fadeContent>
{#if hasArgs}
@@ -143,7 +147,6 @@
disabled={planMode}
{workspace}
prettifyHeader
lightHeader
bind:isValid
bind:args={draft.args}
/>
@@ -163,8 +166,17 @@
</div>
<!-- One region with the actions: these lines report on the run the button below launches,
and separating them would read as two subjects. -->
<div class="flex flex-col gap-2 p-3">
and separating them would read as two subjects. No padding of its own over the fields:
every one of them ends on the room ArgInput keeps for a validation message, and adding
to it would sit the buttons twice as far below the last field as the first label sits
from the top. A form with no fields keeps none of that room, so there it comes back. -->
<div
class={twMerge(
'flex flex-col gap-2 px-3 pb-3',
hasArgs ? '' : 'pt-3',
layout === 'pane' ? 'pl-9' : ''
)}
>
{#if runForm.droppedKeys?.length}
<p class="text-2xs text-secondary">
Not an input of this script, so it will not be sent:
@@ -3,8 +3,6 @@
import { twMerge } from 'tailwind-merge'
import { Button, Tab, Tabs } from '$lib/components/common'
import Toggle from '$lib/components/Toggle.svelte'
import JobStatusIcon from '$lib/components/runs/JobStatusIcon.svelte'
import type { Job } from '$lib/gen'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import { msToReadableTime } from '$lib/utils'
import JobArgs from '$lib/components/JobArgs.svelte'
@@ -170,27 +168,35 @@
chatJob?.durationMs !== undefined ? msToReadableTime(chatJob.durationMs, 2) : ''
)
// The card outlives its job, and sometimes precedes it: a call cancelled before Run never
// had one, and one that failed to start has none either. Synthesizing the shape
// JobStatusIcon discriminates on keeps a single vocabulary of status badges rather than a
// second one for the states only the card knows about.
const statusJob = $derived(
chatJob?.job ??
((canceled
? { canceled: true, success: false }
: failed
? { success: false, canceled: false }
: { running: false }) as unknown as Job)
)
// The badge carries the outcome, so this is only ever how long it took, and 'Not run' where
// there is no time to give because nothing ran.
const statusTime = $derived(
running
? elapsed
: ran
? duration || (failed ? 'Failed' : canceled ? 'Cancelled' : 'Done')
: 'Not run'
)
// The colours the jobs tray paints its status dots (JobsSegment's dotClass), as ink on a
// row that stays transparent: blue running, violet approval, orange queued, green ok, red
// fail. The card outlives its job and sometimes precedes it, so the states only it knows
// about — cancelled before Run, failed to start — read off its own flags instead.
const statusClass = $derived.by(() => {
if (canceled) return 'text-tertiary'
if (failed) return 'text-red-500'
if (!ran) return 'text-tertiary'
switch (chatJob?.status) {
case 'running':
return 'text-blue-500'
case 'suspended':
return 'text-violet-500'
case 'queued':
case 'scheduled':
return 'text-orange-500'
case 'failure':
return 'text-red-500'
case 'success':
return 'text-green-500'
default:
return settled ? 'text-green-500' : 'text-blue-500'
}
})
// How long it took, which is the one thing the colour cannot say. A run that never started
// has no time to give, so its outcome takes the slot — as a word, never "Not run", which
// stutters against the "Run <path>" label beside it.
const outcome = $derived(failed ? 'Failed' : canceled ? 'Cancelled' : 'Done')
const statusTime = $derived(running ? elapsed : duration || outcome)
// What the preview button opens changes with the card: the form while the call is still
// waiting on one, the run once a job exists. Neither, and there is nothing to open, so
@@ -233,13 +239,13 @@
}
</script>
<!-- The run page's own status badge, so a run reads the same wherever it is met, with the
time beside it: the badge says how it went, the number how long it took, and while it
runs that number is still moving. -->
<!-- One readout rather than a badge beside a number: the colour says how the run went and the
text how long it took, which is how the rest of the chat states a status. While it runs
that number is still moving. `font-medium` because the row is a button and the base layer
sets those semibold, which would leave this the one bold word in the header. -->
{#snippet status()}
{#if !pending}
<span class="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap text-2xs text-hint">
<JobStatusIcon job={statusJob} roundedFull size={11} badgeClass="p-1" />
<span class={twMerge('shrink-0 whitespace-nowrap text-2xs font-medium', statusClass)}>
{statusTime}
</span>
{/if}
@@ -274,11 +280,12 @@
{:else if pending}
<RunArgsFormDisplay toolCallId={message.tool_call_id} {runForm} />
{:else}
<!-- One fixed-height region holding the strip and the body, so the card is the same size
on every tab and switching to the raw JSON does not resize it under the cursor. A cap
would not do it — the body is a scroll region, and a max-height silently beats
flex-grow. -->
<div class="relative flex h-[20rem] flex-col">
<!-- One region holding the strip and the body, fixed so the card is the same size on every
tab — a cap would not do it, since the body is a scroll region and a max-height
silently beats flex-grow. The raw view takes that height as a floor instead: its
blocks scroll on their own, as an ordinary tool call's do, so a scroller around them
would be one too many. -->
<div class={twMerge('relative flex flex-col', jsonView ? 'min-h-[20rem]' : 'h-[20rem]')}>
<!-- The tabs go in raw view — they name the parts of the body, and the raw call is not
one of them — while the strip stays, since the JSON toggle lives there. Hence its
fixed height: a row sized by its contents would step every time the tabs leave, and
@@ -323,7 +330,8 @@
use:fades.container
onscroll={fades.measure}
class={twMerge(
'min-h-0 flex-1 overflow-auto px-3 py-2',
'min-h-0 flex-1 px-3 py-2',
jsonView ? '' : 'overflow-auto',
!jsonView && activeTab === 'logs' ? 'bg-surface-secondary/50' : ''
)}
>
@@ -42,8 +42,8 @@
{#if message?.runForm && pending}
<!-- No rule under it, as ArtifactViewer's header has none: the fields scroll under a
fade, and a border would draw that same boundary a second time. -->
<div class="flex items-start gap-2 p-3">
<Code class="mt-0.5 h-4 w-4 shrink-0 text-accent" />
<div class="flex items-center gap-2 p-3">
<Code class="h-4 w-4 shrink-0 text-accent" />
<div class="min-w-0 flex-1">
<p class="truncate text-xs font-semibold text-emphasis">
Run {message.runForm.summary || message.runForm.path}