Files
windmill/frontend/src/lib/autosize.ts
T
hugocasaandClaude Opus 4.8 7ebfad382a feat(ai-agent): give tools a real description instead of the tool name (#10083)
* feat(ai-agent): use a real tool description instead of the tool name

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ai-agent): render tool-name error full width and hoist it above the description

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ai-agent): make tool description field hug its content so a single line is vertically centered

Add an optional minHeight param to the autosize action (default unchanged at 30px) and pass minHeight 0 for the tool description so an empty/one-line field no longer reserves the 30px floor and leaves dead space below the text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(ai-agent): regenerate OpenFlow-derived prompts, CLI guidance, and copilot zod schema for tool description

Fixes the check-freshness CI failure (system_prompts + skills.gen.ts) and makes the flow copilot's openFlow.json / openFlowZod.gen.ts aware of the new AgentTool.description field so AI-authored tools can set it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:33:16 +02:00

158 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { tick } from 'svelte'
type TextArea = HTMLTextAreaElement
/**
* Optional parameters for the `autosize` action.
*
* `maxHeight` caps how tall the textarea may grow. Once the content exceeds it,
* the textarea stops growing and scrolls internally (overflow-y: auto) instead.
* Accepts a number (px) or a CSS-ish string ending in `vh`/`px` (e.g. `'40vh'`).
* When omitted the textarea grows without bound (the historical behaviour).
*
* `minHeight` (px) sets the shortest the textarea may collapse to. Defaults to 30px;
* pass a smaller value (e.g. `0`) for a compact field that hugs a single line of
* content instead of reserving the default floor.
*/
export type AutosizeParams = { maxHeight?: number | string; minHeight?: number } | undefined
/** Resolve a `maxHeight` param to a pixel value, or null when uncapped/invalid. */
function resolveMaxHeight(maxHeight: number | string | undefined): number | null {
if (maxHeight == null) return null
if (typeof maxHeight === 'number') return maxHeight
const s = maxHeight.trim()
const v = parseFloat(s)
if (isNaN(v)) return null
if (s.endsWith('vh')) return (v / 100) * window.innerHeight
return v // 'px' or bare number → pixels
}
export const autosize = (node: TextArea, params?: AutosizeParams) => {
/* ------------------------------------------------------------------
* Constants
* ---------------------------------------------------------------- */
const UPDATE_EVENT = new Event('update')
const DEFAULT_MIN_HEIGHT = 30 // px
const EXTRA = 2 // px added to scrollHeight
let width = 0
let maxHeight = params?.maxHeight
let minHeight = params?.minHeight ?? DEFAULT_MIN_HEIGHT
let capped = maxHeight != null
/* ------------------------------------------------------------------
* Core resize routine
* ---------------------------------------------------------------- */
const resize = () => {
node.style.height = 'auto'
let height = Math.max(node.scrollHeight, minHeight) + EXTRA
const maxPx = resolveMaxHeight(maxHeight)
if (maxPx != null) {
if (height > maxPx) {
height = maxPx
node.style.overflowY = 'auto'
} else {
node.style.overflowY = 'hidden'
}
} else {
// Uncapped — including after a capped→uncapped toggle: drop any inline
// overflow we set while capped so the textarea returns to its default
// (class-driven) behaviour rather than keeping a stale `auto`/`hidden`.
node.style.overflowY = ''
}
node.style.height = `${height}px`
}
/* ------------------------------------------------------------------
* Patch `value` so programmatic changes trigger resize
* ---------------------------------------------------------------- */
const proto = Object.getPrototypeOf(node)
const desc = Object.getOwnPropertyDescriptor(proto, 'value')
if (desc) {
Object.defineProperty(node, 'value', {
get() {
return desc.get?.call(this)
},
set(v: unknown) {
desc.set?.call(this, v)
node.dispatchEvent(UPDATE_EVENT)
}
})
}
/* ------------------------------------------------------------------
* Event listeners
* ---------------------------------------------------------------- */
const onInput = () => node.dispatchEvent(UPDATE_EVENT)
node.addEventListener('input', onInput)
node.addEventListener('update', resize)
// A `vh`-based cap depends on the viewport height, so recompute on window
// resize. Only attached when a cap is configured to avoid adding listeners
// for the many uncapped textareas across the app.
if (capped) {
window.addEventListener('resize', resize)
}
/* ------------------------------------------------------------------
* Inline styling
* ---------------------------------------------------------------- */
node.style.boxSizing = 'border-box'
/* ------------------------------------------------------------------
* Wait for DOM mount, then do an initial measure.
* If the <textarea> is already visible (offsetWidth > 0) this covers it;
* otherwise the first ResizeObserver callback will.
* ---------------------------------------------------------------- */
;(async () => {
await tick()
resize()
})()
/* ------------------------------------------------------------------
* ResizeObserver handles:
* • first time the element gets a real width
* • container/window resizes afterwards
* ---------------------------------------------------------------- */
const ro = new ResizeObserver(([entry]) => {
const newWidth = entry.contentRect.width
if (newWidth !== width) {
width = newWidth
resize()
}
})
ro.observe(node)
/* ------------------------------------------------------------------
* Action lifecycle
* ---------------------------------------------------------------- */
return {
update(newParams?: AutosizeParams) {
maxHeight = newParams?.maxHeight
minHeight = newParams?.minHeight ?? DEFAULT_MIN_HEIGHT
const nowCapped = maxHeight != null
if (nowCapped && !capped) {
window.addEventListener('resize', resize)
} else if (!nowCapped && capped) {
window.removeEventListener('resize', resize)
}
capped = nowCapped
resize()
},
destroy() {
ro.disconnect()
node.removeEventListener('input', onInput)
node.removeEventListener('update', resize)
if (capped) {
window.removeEventListener('resize', resize)
}
}
}
}
export default autosize