* chore: add dev server supervisor and dev-only polling dormancy
* fix: address review findings in dev supervisor
* fix: support https mode and bound the idle reaper in dev supervisor
* fix: persist dormancy install guard and hold the reaper during startup
* chore: run worktree frontends under the dev supervisor
* fix: keep app websockets working and reap children on sighup
* feat(sessions): persist artifact version selection in preview tabs
The artifact viewer's version pin was component-local state, so picking an
older version from the history dropdown was lost on reload. It now rides on
the preview tab's URL (`artifact:<id>?v=<n>#<name>`), which is persisted with
the tab, so a reload lands the reader back on the version they were reading.
Omitting a version means "leave the reader where they are", not "show the
latest". Every artifact tool re-opens the document it just wrote, so an
omitted version that cleared the pin would yank a reader out of the version
they chose on every single edit. That rule lives in keptVersion(), which
targetUrl() applies to every path that re-points a tab, so open() and
navigate() cannot disagree about it — the breadcrumb picker opens highlighting
the artifact the active tab already shows, and re-picking it must not double
as a reset to latest. A pin belongs to a (tab, artifact) pair, so a tab
re-pointed at a different document carries nothing over, and a new tab starts
unpinned. Moving off a pin is the reader's own action, through the version
dropdown, "Back to latest", or the new pinArtifactVersion(). Since the pin is
part of the tab model, get_preview_status now reports it, so the assistant can
tell that the reader is not looking at what it just wrote.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(sessions): bound a stamped artifact version to a safe integer
Number.isInteger(1e21) is true, but interpolating it yields `?v=1e+21` while
parseArtifactRoute matches digits only, so artifactUrl could stamp a url that
reads back as null — the one outcome the guard exists to prevent, and one that
would persist with the tab. Safe integers always interpolate in decimal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(artifacts): tell a failed version read apart from a missing version
getArtifactVersion swallowed a rejected read and returned undefined, so a
transient IndexedDB failure was indistinguishable from a pruned snapshot. Both
its callers act on that distinction, and both acted wrongly: the artifact
viewer clears the reader's pinned version on absence — now that the pin is
persisted with the tab, clearing it destroys it — and read_artifact tells the
model the version is gone and to call list_artifact_versions.
It now rejects instead. The store still answers for the current version, which
it holds in memory and can serve without the DB; anything older propagates, the
viewer keeps the pin and leaves the document on screen, and read_artifact
reports a read it could not make rather than a version that does not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): use the Password component on the login and reset-password forms
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): submit auth forms once per Enter keypress
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): conceal revealed password before submitting auth forms
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: stop the AI chat destroying secret variables on edit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: clear stale staged secret values and state the draft-staging rule
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: condense the pending-secret invariant to its field
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: refuse empty and oauth-managed secret values, keep drawer-staged ones in the draft
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: resolve a variable deploy's secret from one draft snapshot
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: make the variable draft the single source of a staged secret
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: drop stale in-memory secret invariants from comments and the eval
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: stop null account/expires_at leaking into variable drafts and diffs
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: report when a variable deploy leaves the secret value unchanged
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: scope the variable-value readability claims to the chat
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: correct the secret-draft invariant in the diff masking comment
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: record why a non-secret value is resent on a partial update
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: stop "Load secret value" discarding a staged secret
The audit-logged load writes the deployed secret into the draft row the
variable drawer shares with the AI chat, so offering it while that row
already stages a value silently replaces it — and the deploy that follows
carries the old value with no sign the staged one was lost.
The gate that hid the action already existed but keyed on
`isEncryptedDraftValue`, which only holds once a draft has round-tripped
through the server. A value staged in the same tab is still plaintext, so
it slipped through. Key on "anything staged" instead; clearing stays
explicit via Reset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: extend the variable draft's empty-value sentinel past secrets
Two gaps in the chat's variable write path, both from treating "the draft
cannot carry this value" as meaning only "the value is secret".
`variableToDraftState` drops the value of an OAuth-managed variable so a
refreshed live token is never pinned into a draft, leaving '' behind. The
deploy body resent that '' verbatim for a non-secret one, wiping the token
the refresh flow owns. The sentinel now covers every value the draft is not
allowed to hold, which also removes the divergence from
`VariableEditor.save` and the shared deployer.
Making a variable secret when it holds no value produced a secret draft
staging '', a deploy body with no `value`, and the backend's "cannot change
is_secret without updating value too" — the sibling create path already
answers that case with guidance, so answer it here too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: gate the Secret toggle's secret load on the staged value too
The toggle calls `onLoadSecret` on every change so an is_secret flip has a
value to send, but that load overwrites the shared draft row — the same
discard the button gate just closed, reached by a different control.
It now loads only when the row stages nothing, which is exactly when the
flip needs a value fetched. With a value already staged there is one to
send, and it is the one the user or the chat put there.
Blocking the load costs the side effect that used to mask a worse bug: for
a deployed variable, the load replaced an `$encrypted:` marker with real
plaintext before save. Without it, un-securing a marker would store the
marker string as the value, since the deploy endpoints only decrypt it while
is_secret stays true. So the toggle is disabled outright while a marker is
staged — Reset first. That closes the marker case for draft-only variables
as well, where no load could ever have masked it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: expose every runs filter on the open_page chat tool
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: reject runs filters the page would silently ignore
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: normalize runs list filters and refuse combinations the page drops
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: validate the full folder-name contract and pin evals to one call
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: refuse queue statuses the concurrency view cannot filter on
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: avoid content shift on home page load and in the script editor logs pane
The tutorial banner rendered by default and was removed once an API round-trip
resolved that it should not show, jumping everything below it up by 58px on
every home page load. It now caches the last resolved state in localStorage and
paints that first, so the first frame already matches what the sync concludes; a
device with nothing cached stays hidden until the sync answers.
The logs header spinner was an unsized lucide icon (24px) where the settled
state renders a 12px Timer, so the row grew 7px while a job was queued and
shrank back when it started, shoving the log body down and up again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the tutorial banner hidden when dismissed mid-sync
The banner is interactive while the initial tutorial-progress request is still
in flight, so a dismiss or a skip can land before the sync resolves. The
continuation then overwrote the user's choice and brought the banner back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: pin the result placeholder row height across the spinner swap
Sizing the spinner to the font size still left it 6px short of the text-sm line
box it replaces, so the row contracted instead of growing. Pin the height on the
container so it holds in both states and tracks the root font size.
Also assign state before persisting it, and collapse the duplicated rationale
above the banner cache.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: stop the test panel splitpanes resting one header too tall
The panes carried `!max-h-[calc(100%-{...}px)]`, but the arbitrary value is
built by string interpolation so Tailwind never emitted a rule for it: the
class was inert and the computed max-height was `none`. The panes then took
their 100% height, ignoring the header row above them, and overflowed the
column by exactly the header. Flex only applied the shrink transiently, so a
reflow during a run snapped the whole logs & result region up ~12px and back.
min-h-0 lets flex size the panes to the space that is actually left, which is
what the clamp was reaching for and is correct for the debug and bottom layouts
too, without their hardcoded 83/43/0 pixel guesses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): declutter the workspace picker and workspace creation
* fix(frontend): fail open when the auto-invite domain check errors
* fix(frontend): fail closed when the auto-invite domain check errors
* feat(frontend): present prod and dev as sibling choices on the workspace card
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(ata): prefer the npm proxy when the instance configures a registry
* fix(npm-proxy): cap tarball extraction and stop pinning a failed config probe
* fix(npm-proxy): keep large packages cacheable by using a single shard
* fix(npm-proxy): cache the archive so a large package is served, not refused
* fix(npm-proxy): read archives off the runtime, keeping only what types need
* fix(npm-proxy): charge a retained entry for what it allocates, not its bytes
* fix(npm-proxy): size retention for real packages and read the manifest back
* fix(npm-proxy): charge path bytes and pin the manifest read-back
* fix(npm-proxy): stop retaining past the budget instead of refusing the package
* fix(parser-py): keep first param when def main( line has trailing comment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump windmill-parser-wasm-py to 1.782.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(ata): fall back to the npm proxy when the CDN request fails outright
* fix(ata): surface proxy failures and guard the body read too
* docs(ata): state the proxy catch's constraint, not its history
* fix(ata): log a failed proxy d.ts fetch, which callers discard
* feat(raw-apps): route in-browser npm installs through the npm proxy
* fix(npm-proxy): follow npm range semantics and cache packuments
* fix(npm-proxy): bound the packument cache by bytes and stream tarballs
* fix(npm-proxy): keep a v-prefixed pin exact and read the tarball once
* chore(raw-apps): bump the ui_builder pin to the npm-proxy installer
* feat(triggers): nested any_of / all_of filter groups
A trigger filter entry can now be a group — `{"any_of": [...]}` or
`{"all_of": [...]}` — nesting further entries, so criteria like
`A AND B AND (C OR D)` are expressible. Existing flat `{key, value}` lists keep
their meaning, combined by the trigger's `filter_logic` as before.
Filters are compiled once per connection: the set of top-level keys the whole
tree references is collected up front, so a message is parsed in a single
streaming pass that captures only those keys, instead of one full pass per leaf
filter as before. Filters that fail to parse are now logged rather than dropped
silently, since a nested group is easier to mistype than a flat entry.
The editor gains "Add group", rendering groups recursively with their own
AND/OR selector; Kafka and WebSocket triggers share it.
Fixes WIN-2345
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(triggers): drop empty filter groups instead of evaluating them
A group with no criterion cannot evaluate to a constant: true makes an `or`
filter list accept every message, false mutes an `and` list. Two clicks in the
editor ("Add group", save) produced one. Drop it when compiling so its siblings
stay in force, and reject at save time the filters the listener would otherwise
drop silently.
Also restore the item shape of `$ref`-typed arrays in the generated agent
schemas: the extractor only resolved refs at the property level, so moving
`filters.items` to a shared schema flattened it to a bare object. Resolving them
inside `items` too also recovers the shapes `initial_messages` and the MQTT
`topics` had already lost.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(triggers): name the offending entry when a nested filter is invalid
Serde's untagged error only reports that the outermost entry matched no
variant, whatever depth is actually wrong, which defeats the point of
validating a group at save time. Walk the tree instead and report the path.
Normalize the WebSocket editor's filters to [] on load, as the Kafka editor
does, so the list component can rely on an array.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(triggers): key filter rows by node so deletion keeps values aligned
The value editor seeds itself from `code` once, so an index-keyed row reused
for a different filter kept showing the deleted row's value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: bump ee-repo-ref after merging main
The merge pulled OSS code that needs EE symbols newer than the companion
branch's base, so the companion was merged with EE main too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* perf(triggers): keep filter short-circuiting from materializing unread fields
The single-pass scan deserialized every referenced key before the boolean tree
ran, so an AND whose first leaf rejects the message still allocated the large
objects the later leaves name — the shape this feature exists for. Borrow the
wanted keys as raw slices during the scan and parse a field only when
evaluation actually reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(triggers): none_of filter group
Negation of a nested group, so a trigger can exclude what it must not react to
without inverting every other criterion. A key the message does not carry
satisfies it: there is nothing there to match.
Only groups can negate — the root's operator is the trigger's filter_logic
column, which has no value for it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(triggers): address a nested field with a dotted path
`{path: "a.b.c", value: v}` alongside the existing `{key, value}`, so the common
case reads the way people write it instead of nesting the shape into the value.
A separate field rather than dots in `key`, which already means the top-level
field spelled that way — overloading it would resettle what existing triggers
over flattened payloads match.
Paths address objects only for now: a path through an array does not match
rather than guessing an element, and array containment stays on the value side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(triggers): mention none_of in the filter_logic description
Plus a test for the empty-path-segment rejection, which had none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 78859aab0c6e78283ec8d2b37e8c410963afdc83
This commit updates the EE repository reference after PR #722 was merged in windmill-ee-private.
Previous ee-repo-ref: 0e42ba72ccc38a6b0a380f58afe0db36d284f4c9
New ee-repo-ref: 78859aab0c6e78283ec8d2b37e8c410963afdc83
Automated by sync-ee-ref workflow.
* fix(triggers): reject a criterion naming both key and path
The untagged enum takes such an entry as a `key` criterion and drops the
`path`, which is the silent-ignore the save-time validation exists to prevent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(triggers): drop the label next to the key/path toggle
The toggle already shows which one is selected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(triggers): reject an entry that combines a criterion with a group
Generalizes the key+path fix: the untagged enum settles a half-and-half entry
on the first variant that fits and ignores the rest, so a criterion carrying a
group key lost the whole subtree without a word.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: version resource values with history, diff and restore
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: record resource versions in a trigger so direct writes are covered
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: show the selected version's value and tighten history write access
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* perf: gate resource version recording in trigger WHEN clauses
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: clear a resource's past versions, and address review nits
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: restore the displayed version and keep author attribution on pooled writes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: scope history to the selected workspace and gate clearing on ownership
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: gate restore on write access and clearing on the signed-in workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): share the version-history row between script and resource drawers
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* perf: trim resource version history in the monitor sweep, not on write
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): match the script versions drawer shell for resource history
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* perf(frontend): highlight version values instead of mounting monaco
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): match the script drawer's code preview presentation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: rank version trim in one windowed pass instead of a correlated delete
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): treat the newest version as current by position
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* perf: gate the resource version trim to an hourly sweep
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: unnest the version row action and correct the trim cadence docs
* perf: cap the history listing and use sets for reference lookup
* feat: warn when a resource is written more than 60 times a minute
* fix: lower the resource write advisory to 20 per minute
* fix: discard stale history loads and never diff against an unread value
* fix: correct the write advisory boundary and document the eviction lock
* fix: read history and the live value from one snapshot
* refactor: read the drawer's diff baseline from versions, not the live resource
* fix: open the history drawer with no version selected
* fix: disarm the clear confirmation and clear the pane when the selection moves
* fix: explain the missing diff and drop a guard that can no longer fire
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(smtp): explain why a test email failed instead of 'deadline has elapsed'
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(smtp): keep non-SMTP error codes and retire a stale test alert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to f0df8b82c4c089d384423ed64b8504506084820d
This commit updates the EE repository reference after PR #721 was merged in windmill-ee-private.
Previous ee-repo-ref: 1ffaf3dea81e007c6c11146c1e12e97e83f5b938
New ee-repo-ref: f0df8b82c4c089d384423ed64b8504506084820d
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix(duckdb): cast columns in quicksearch so nested types can be previewed
DuckDB's `CONCAT` implicitly casts scalars but rejects nested types:
D SELECT CONCAT(' ', ['a','b']);
Binder Error: Cannot concatenate types VARCHAR and VARCHAR[] - an explicit
cast is required
Quicksearch concatenates every visible column, so one LIST, STRUCT or MAP column
makes a table impossible to preview — both the grid and its row count fail:
Binder Error: Cannot concatenate types VARCHAR, VARCHAR, BIGINT, ...,
VARCHAR[], ... and TIMESTAMP WITH TIME ZONE - an explicit cast is required
LINE 1: ... FROM "raw"."accounts" WHERE ($1 = '' OR CONCAT(' ', "id", ...
Every scalar in that list concatenates fine on its own — VARCHAR, BIGINT,
DOUBLE, BOOLEAN, DATE and TIMESTAMPTZ were each checked individually — so the
array column is the entire cause.
Cast each column in the predicate. The comparison is textual either way, so no
result changes, and the projection is untouched: casting there would change the
types the caller reads back. This follows the shape already used for MSSQL in
`mssql_needs_cast_for_eq`.
Both DuckDB quicksearch sites are covered, SELECT and COUNT. Fixing one leaves
the grid rendering while the row count still errors.
Tests include the live path: the Database Manager sends a
`-- WM_INTERNAL_DB_SELECT {...}` marker and the backend expands it, so the new
test drives that expansion with the real 27-column definition captured from a
failing job, `sync_id VARCHAR[]` included. It fails without the fix and passes
with it.
* fix(frontend): cast columns in the DuckDB quicksearch
Same defect as the Rust query builders, in the implementation that actually
runs. `make_select_query` / `make_count_query` in windmill-common have no callers
anywhere in the repo; the query the browser sends is built here.
DuckDB's CONCAT implicitly casts scalars but rejects nested types, and
quicksearch concatenates every visible column, so one LIST column makes a table
impossible to preview — both the page and its row count fail with
Binder Error: Cannot concatenate types VARCHAR, ..., VARCHAR[], ... and
TIMESTAMP WITH TIME ZONE - an explicit cast is required
The helper lives in select.ts and is imported by count.ts so the two cannot
drift, and both call sites are fixed: fixing only SELECT leaves the grid
rendering while the row count still errors.
* fix(duckdb): cast only list columns in quicksearch, leaving other SQL byte-identical
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(frontend): pin the DuckDB quicksearch column list byte-for-byte
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: stop the new raw app modal claiming AI is unconfigured before it knows
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: require a resolved workspace before trusting the loaded AI config
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: drop the unreachable token guard and point superadmins at instance settings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: show the instance settings link to superadmins who are not workspace admins
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: never replace an in-flight indexeddb open, only a settled one
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: keep a version history for session artifacts
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: let the assistant browse an artifact's earlier versions
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: pick an older artifact version from the preview panel
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(ai_evals): cover the change note the assistant writes on each edit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: bound every indexeddb open, not only one told it is blocked
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): call a dev workspace a dev workspace in the merge UI
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): drop the unreachable dev-workspace guard on the fork modals
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 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>
The consumer name field in the NATS trigger config read "Required is using
JetStream" instead of "Required if using JetStream", matching the wording
already used by the sibling stream name field.
Fixes WIN-2335
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: allow custom dev workspace environment labels
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: reject dev labels that shadow a tracked branch's namespace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: guard dev labels against a repo's assumed default branch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: state the badge-cap rationale once and drop unenforceable openapi constraints
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: offer a fixed list of environment labels instead of free text
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: match the accepted label set to the openapi enum exactly
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: stop describing the label set as dev/staging only
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): hide the fork workspace banner from operators
* fix(frontend): scope the operator gate to the workspace its role was fetched for
* fix(frontend): drop superseded whoami responses instead of writing a stale role
* fix(frontend): guard the remaining workspace-switch userStore writers
* feat: open a session edit in the preview panel from the edits list
* refactor: move the deploy-kind preview mapping next to its siblings
* feat: make preview the primary action in the session edits list
Clicking a row in the Edits popover now opens the item in the session
preview panel; kinds the panel cannot host fall back to their diff. Each
row gains an explicit Diff button for that item, and a pinned footer row
opens Review & deploy for the whole set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: fold the open-and-flash rule into SessionPreviewTabs
The rule for when a preview open should flash the tab was written out at
three call sites, one of them with a looser condition. Move it onto the
tab owner as openAndPulse so the three agree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: preview data-pipeline edits from the edits list
A pipeline bundle is stored at `f/<folder>/data_pipeline` while its editor
is the folder's pipeline view, so the deploy-kind mapping has to route on
the folder. Without it the one remaining previewable kind fell through to
the drawer. Also pin the open-and-flash rule with tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: state the pipeline bundle path layout once
`f/<folder>/data_pipeline` was parsed independently in the compare page,
the home list and the session preview mapping — two of them disagreeing on
whether the trailing segment is checked — and built by hand in the editor.
Route all four through $lib/pipelinePaths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: route the pipeline page through the shared bundle path
The route built the bundle path by hand and passed the draft kind as a
literal, the two halves of the editor disagreeing on where the layout is
stated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: name a pipeline edit by its folder in the edits list
The bundle path is an implementation detail and the row's click lands on
the folder's editor, so showing `f/<folder>/data_pipeline` named something
the click doesn't open. Also derive the bundle regex from the draft-kind
const it has to agree with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore(cli): drop unrelated package-lock.json change
The lockfile diff was an incidental regeneration from an older manifest
(it downgraded the locked svelte range below what cli/package.json
requires) and had nothing to do with this PR. Reset to main's version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* refactor(recordings): build recordings from the completed run instead of live event capture
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(recordings): budget and isolate replay synthesis against hostile recordings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(recordings): capture full logs and the run-time flow, upgrade v1 files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(recordings): pick an existing run for the hub recording
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(recordings): cap recorded logs under the replay loader's text budget
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(recordings): pin picked runs to their executed version, keep v1 streamed logs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(recordings): bound pipeline finalize fan-out, pin flow schema to run version
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(recordings): warn on mixed-version fallback, bound code fetches
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: add public sharing option for job pages
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: gate public run sharing and address review findings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: address review nits on public run sharing
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: key public run view on workspace, job and token
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the mermaid fullscreen dialog inside its pane and its emoji vector
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: load only the fonts a diagram needs and size chrome per breakpoint
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: cover every emoji class in the font preload without restyling diagrams
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the dialog chrome allowance in rem so it scales with the root font
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: decode mermaid entity codes when sampling text for the font preload
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: match mermaid's decimal-only entity codes and decode the Inter sample too
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: drop emoji format characters from the font sample
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: state the emoji subset spread and modifier exclusions precisely
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: re-render once fonts settle instead of hand-picking emoji subsets
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: give Modal a fill-height mode instead of measuring its chrome
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: guard the post-fonts re-render against a newer render
The re-render after document.fonts.ready assigned svg without re-checking
renderSeq after its own await. renderedCode is set before that await, so a
stale re-render landing last leaves svg holding the previous diagram while
renderedCode names the current source — showSvg stays true and paints the
wrong diagram.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: compact ai chat context for models with unknown context windows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: correct stale comment on unknown-model context window handling
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: surface assumed context window in usage indicator for unlisted models
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#10560 changed the script editor's change handler to read `e.detail` instead of
`editorCode`, on the reasoning that the two only diverge while the editor is
being torn down. They also diverge in a live editor: `setCode` dispatches the
string it was handed, but `alignCodeWithEditor` applies that string to Monaco
first, and the resulting `onDidChangeModelContent` runs `updateCode`
re-entrantly — so if the model normalized the text (EOL is the reachable case;
`ScriptBuilder` builds template content with a `\r\n` join), `editorCode`
already holds the buffer's version and the payload is the pre-normalization
one. Taking the payload leaves `code` disagreeing with what the editor shows,
which the external-write effect then tries to reconcile on every change.
The language-switch fix in that PR is `alignCodeWithEditor` clearing the timer
its own write armed; that part stands and is unaffected. This restores the
handler to the buffer-true read.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>