mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
* 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>
158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
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
|