Files
windmill/frontend/src/lib/components/flows/flowPanelMode.svelte.ts
T
GuilhemandClaude Opus 5 676256bacc feat(flow-editor): measure step panel placement (#10543)
* feat(flow-editor): measure the redesigned step panels

Instruments the flow editor's step, loop and branch panels on the existing
anonymous `feature_usage` channel, so the redesign can be judged on how the
panels are actually used rather than on nothing.

Eight event kinds under a new `flow_editor` feature: panel opens and their
dwell (bucketed, per placement), placement-preference overrides, which
settings get configured or cleared, settings that read as invalid, the
prop-picker connect lifecycle, AI input suggestions, and the step header
menu that "Save to workspace" now lives behind.

Settings changes are diffed off `describeStepSettings`, the same view the
graph badges render, so the telemetry vocabulary cannot drift from the one
on screen. Only `panel_open` and `setting` carry an entity id — one opaque
id per editor mount — since a per-entity row is only worth its cost where
the spread per editing session is the question.

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

* fix(flow-editor): keep the panel telemetry honest

Review follow-ups on the instrumentation:

- The top dwell bucket was `120s+`, and `+` is outside the charset
  `is_identifier_shaped` accepts, so `log_feature_usage` skipped those
  events and still answered 204 — the longest visits vanished with no
  error on either side. Renamed to `120s_plus` and pinned every emittable
  key against the backend's charset in a test, since the producer is
  TypeScript and the validator is Rust.
- Dropped the per-session entity id from `setting`: it would pay a row per
  session per day across twenty-four keys, for a distribution its plain
  counter already largely answers.
- An armed connect that went away with its component never reported, so
  `open` did not balance against `insert` + `abandon`.
- Session preview tabs keep hidden editors mounted, which billed panel
  time nobody spent. `FlowEditorView` now publishes the visibility it
  already knows about.
- Re-picking the active placement row logged a move, which also made
  `auto:from_docked` mean two different things.
- The last dwell of a session was lost on tab close, since Svelte tears
  components down on navigation but not on `pagehide`.

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

* refactor(flow-editor): narrow the telemetry to panel placement

The eight-kind instrumentation measured more than could be read. With nothing
recorded before the redesign there is no baseline to compare panel opens, dwell
times, settings usage or connect funnels against, so those counters answered
questions nobody could act on while costing a row per key per day in an
instance-wide table.

What remains are the three numbers the modal panel is actually judged on: how
often the 1280px breakpoint puts the panel in a modal, and how often people
override that in each direction.

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

* fix(flow-editor): stop counting placement in session preview tabs

Preview tabs keep every flow editor mounted and laid out at panel width
whether or not it is the visible one, and that panel is narrower than the
breakpoint by construction. Each flow tab opened in a session therefore
emitted a `breakpoint_modal` on mount, and one drag of the session panel
across 1280px emitted one per mounted tab — with no host dimension in the
key to separate that from the crossings the counter exists to measure.

Also corrects the comment on the no-op placement guard, which justified
itself with a key vocabulary that no longer exists.

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

* fix(flow-editor): make the three placement counters comparable

Sessions were excluded from the breakpoint counter but not from the two
override counters, so a pin made in a session landed in the same bucket
used to judge the breakpoint, with no crossing in the denominator to read
it against. All three are now gated together.

An override is also only counted when it moves the panel. Choosing
"Detached" on an editor the width had already put in a modal states a
preference without changing anything, and the aggregate carries no width
to separate that from the wide-screen override that is the actual signal.

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

* docs(flow-editor): describe the two override keys by what emits them

They documented themselves as overriding `auto`, which is no longer the
rule: pinning Attached on a wide editor overrides `auto` and emits
nothing, while going from an Attached pin to Detached below the
breakpoint emits `force_detach` even though `auto` would have produced a
modal there too. This file is what someone reads when interpreting the
numbers, and "override of auto" is the misreading the emission rule
exists to prevent.

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

* fix(flow-editor): count the panel moving, not the breakpoint being armed

The tracker held "the breakpoint is responsible for this modal" rather
than "the panel is modal", so on a narrow editor pinning Detached and
releasing it back to Auto emitted a second breakpoint_modal for a panel
that never moved. It also died with the editor, which FlowBuilder rebuilds
through a `{#key}` on every reload — each rebuild re-armed it and counted
the same narrow editor again.

Both inflate the denominator that the two override counters are read
against, and both bias it the same way: toward concluding that nobody
overrides the breakpoint.

The tracker now follows the panel's placement across preference changes,
and FlowBuilder owns it from above the `{#key}`, which also puts the
session exclusion in one place instead of at each call site. The
moves-only rule moves into `forcedPlacementEvent` so both halves of it sit
in the module the tests can reach.

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

* fix(flow-editor): ignore placements measured before the editor is laid out

A reload rebuilds the editor through `{#key renderCount}`, and the panel
controller is rebuilt with it: its width restarts at zero, which resolves to
`docked` because that is what is safe to render rather than because the editor
is wide. The breakpoint tracker read that transient as the panel having docked
and counted the real width landing as a fresh crossing, inflating the
denominator both override ratios are read against.

`useFlowPanelMode` now exposes `measured`, and the tracker skips anything
unmeasured instead of recording it as a placement.

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

* docs(flow-editor): state the placement invariants once each

The width-zero rule had accumulated at four sites, two of which forward it
without being able to break it. Keep it beside the guards that enforce it.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 12:29:31 +02:00

36 lines
1.2 KiB
TypeScript

import { resolvePanelMode, type FlowPanelMode, type FlowPanelPreference } from './panelPlacement'
/**
* Holds the step panel's placement preference and the editor's measured width, and reads
* the resolution off `resolvePanelMode`. The preference is not persisted: it lasts as long
* as the editor is open, so every flow opens on `auto` and a pin is a deliberate act each
* time.
*/
export function useFlowPanelMode(opts: { enabled: () => boolean }) {
let preference = $state<FlowPanelPreference>('auto')
let width = $state(0)
return {
get preference(): FlowPanelPreference {
return preference
},
set preference(next: FlowPanelPreference) {
preference = next
},
get mode(): FlowPanelMode {
return resolvePanelMode({ enabled: opts.enabled(), preference, width })
},
/**
* Whether `mode` reflects a real layout. Until the first measurement lands, `mode` is
* `docked` because that is the safe thing to render, not because the editor is wide.
*/
get measured(): boolean {
return width > 0
},
/** Fed by the editor root's measured width; drives `auto` in both directions. */
measure(measured: number | null | undefined) {
width = measured ?? 0
}
}
}