Commit Graph

8183 Commits

Author SHA1 Message Date
Amey Pawar 44f5dd6636 fix(frontend): respect forced column order for numeric column names (#9463) 2026-06-07 07:55:12 +00:00
Ruben Fiszel 004339032e chore(main): release 1.719.0 (#9459)
* chore(main): release 1.719.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-06 08:16:43 +00:00
Ruben Fiszel 3887bf67dc chore(main): release 1.718.0 (#9450)
* chore(main): release 1.718.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-05 14:55:10 +00:00
Ryan Gray 90677872f6 fix: distinguish canceled jobs in runs (#9452)
* fix: distinguish canceled jobs in runs

* fix: order status=failure|canceled by completed_at to use partial index

The new `status` query param replaced the legacy `success=false` filter on
the Runs page, but the ORDER BY switch in list_completed_jobs_query only
flipped to v2_job_completed.completed_at for success==Some(false). With
status=failure|canceled (and success=None), the query fell back to ordering
by v2_job.created_at, which the partial index
ix_v2_job_completed_failure_workspace (workspace_id, completed_at DESC WHERE
status IN ('failure','canceled')) cannot serve.

EXPLAIN ANALYZE on 500k rows (1% failure/canceled): ordering by completed_at
uses the partial index (~150 buffers, 0.3ms); ordering by created_at scans
the v2_job created_at index and probes/discards 99% of rows via the join
(~49k buffers, 31ms). Switch the ordering to completed_at for
failure/canceled so the partial index serves both filtering and ordering.

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

* test: trim order-by regression test to the failure/canceled case

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

* fix: only treat canceled as a terminal status icon for completed jobs

Guard the canceled branch in JobStatusIcon and getJobStatusKind with
`'success' in job` so a job that is still running while being canceled keeps
its running icon/favicon until it completes, instead of immediately showing
the gray Canceled state. Also clarify the openapi `status` param is an exact
match (status=success excludes skipped, unlike success=true).

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

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:50:07 +00:00
Ruben Fiszel 7590b28108 feat(sandbox): pull/extract images with crane instead of podman (#9455)
* feat(sandbox): pull/extract images with crane instead of podman (+ add to image)

The sandboxed container runtime (`# sandbox <image>`) only ever pulls + flattens an
image (nsjail does the run), so a full container engine is overkill — and podman was
never actually in any Dockerfile, so the merged feature couldn't run in the shipped
image. Switch to crane (google/go-containerregistry): a single ~25MB static binary,
no daemon/store/root/privileged.

- docker_v2.rs: crane export -> flattened rootfs tar, crane config -> OCI config,
  crane digest -> content-addressed rootfs+config cache (cross-job dedup + automatic
  freshness), crane manifest -> pre-download size guard. DOCKER_CONFIG authfile dir.
  Cache eviction prunes the rootfs-tar cache by mtime (LRU). Pull policy honored via a
  ref->digest cache (missing/never reuse without a registry hit).
- Dockerfile + docker/DockerfileSlim{,Ee}: install the crane binary (Full/FullEe and
  the EE image inherit it via FROM the base image).
- docs + UI text + instance-setting descriptions updated (download size is compressed;
  cache is the rootfs-tar cache).

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

* fix(sandbox): address CI review — digest-pinned fetch, size cap on every job, eviction race

Codex P1s:
- Fetch by the resolved digest (name@digest), not the mutable tag, so content can't
  diverge from the digest the cache is keyed under if a tag moves mid-fetch.
- Enforce the size cap on EVERY job via a cached {digest}.size sidecar (no registry call
  on cache reuse), so lowering the limit rejects already-cached oversized images.
- Eviction race: hardlink the cache tar into the job dir before tar -xf (pins the inode
  against concurrent eviction) and re-fetch if it was evicted first.
Claude P2s: atomic config sidecar (tmp+rename) + tolerate torn parse; soften the LRU
comment (mtime = creation order); sweep orphaned *.tmp.* and .size on eviction.
+digest_key/ref_key unit tests.

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

* fix(sandbox): P1 cross-fs cache staging (EXDEV), Dockerfile arch fail-fast

CI re-review (Claude + Codex P1): the eviction-race hardlink crosses filesystems in the
shipped deployments — the cache is its own volume (/tmp/windmill/cache) while the job dir
is on the container fs — so hard_link returns EXDEV (not NotFound) and every sandbox job
fails. Fall back to tokio::fs::copy on a non-NotFound link error; copy reads through the
source inode so it still survives a concurrent eviction.
Also: Dockerfiles fail fast with a clear error on an unsupported arch instead of building
a 404 crane URL; ref->digest file written via tmp+rename (no torn read under missing/never).

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

* docs(sandbox): say 'oldest by creation time' not 'LRU' for cache eviction

Codex P2: the code evicts by tar creation time (cache hits don't touch mtime), so the
user-facing docs + instance-setting text shouldn't claim true LRU.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:52:55 +00:00
Ruben Fiszel 1727271e19 feat: sandboxed daemonless container runtime via '# sandbox <image>' (#9453)
* feat: add sandboxed docker v2 runtime via '# docker <image>'

Run a container image as a subprogram of the job's own nsjail sandbox:
extract the image rootfs with podman (rootless) and run it chrooted inside the
job's nsjail, so the container inherits the job's confinement and is safe under
nsjail / for untrusted code. Selected by '# docker <image>'; a bare '# docker'
keeps the v1 (dind) path untouched.

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

* feat: default to daemonless docker (drop dind from compose, allow docker on cloud)

docker-compose no longer ships the dind sidecar (v2 is daemonless: podman + nsjail
in the worker); removed the dind service, DOCKER_HOST env, depends_on and volume.
Removed the language-picker guard that blocked Docker scripts on the multi-tenant
platform, now that v2 makes docker safe to run sandboxed.

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

* feat: select sandboxed container via # sandbox <image>; add pull policy + size guards

- Surface moved from '# docker <image>' to '# sandbox <image>' (groups under the
  sandbox annotation; '# docker' stays v1-only, '# sandbox' stays nsjail-bash).
- SANDBOX_IMAGE_PULL_POLICY (default 'newer') so moving tags don't go stale.
- SANDBOX_IMAGE_MAX_SIZE_MB rejects oversized images before extraction.
- SANDBOX_IMAGE_CACHE_MAX_MB best-effort LRU eviction of podman's image store.

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

* feat(sandbox): support # volume, honor nsjail tmp instance settings, v2 docker template

- Thread shared_mount into the sandbox container nsjail config so '# volume' mounts
  (and the same-worker /tmp/shared folder) apply inside the container.
- Use resolve_nsjail_tmp_mount_block for the container's /tmp so it honors the same
  nsjail_tmp_backing / nsjail_tmpfs_size_mb instance settings as other nsjail jobs.
- docker-compose comment + the editor's Docker template now use '# sandbox <image>'.

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

* feat(sandbox): make image size/cache/pull-policy UI instance settings

Convert SANDBOX_IMAGE_* from worker env vars to DB-backed instance settings
(sandbox_image_max_size_mb, sandbox_image_cache_max_mb, sandbox_image_pull_policy),
hot-reloaded via the same mechanism as nsjail_tmpfs_size_mb and configurable in
#superadmin-settings. No worker restart needed.

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

* feat(sandbox): windmill-managed registry — default registry + private auth

Two new instance settings:
- sandbox_image_default_registry: prepended to unqualified image refs (alpine ->
  <registry>/alpine); fully-qualified refs untouched.
- sandbox_registry_auth: docker/podman auth.json blob written to a per-job authfile
  (0600, removed with the job) and passed to podman --authfile for private registries.
Both hot-reloaded and configurable in #superadmin-settings.

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

* fix(sandbox): protobuf-safe proto_str escaper, atomic 0600 authfile, registry tests

Addresses local-review P2s: proto_str now emits valid protobuf octal escapes for
control/non-ASCII bytes (not Rust \u{..} that nsjail would reject); the registry
authfile is created 0600 atomically (no world-readable window); add a
registry_qualified table test + a non-ASCII proto_str case.

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

* fix(sandbox): P0 — deliver image env via nsjail envar:, never the launcher process env

CI review (P0): the image's OCI Env (attacker-controlled keys+values) was applied to
the nsjail launcher process via .envs(), so a hostile image could set LD_PRELOAD/
LD_LIBRARY_PATH/LD_AUDIT on nsjail itself and execute code as the worker outside the
jail. Now the image env is rendered as proto-escaped 'envar:' directives (child-only)
and nsjail's process env carries only windmill-trusted keys (reserved vars + proxy).
Also: warn instead of silently bypassing the size guard on inspect failure; reset the
eviction guard via a Drop guard (no stuck flag on panic/early-return). +render_envars test.

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

* fix(sandbox): P0 symlink-write escape via rootfs script; P1 redact registry-auth logging

CI review:
- P0 (Codex): the body was written into the image-controlled rootfs as
  .windmill_docker_main.sh via write_file (follows symlinks) — a hostile image could
  plant that path as a symlink to a host file and capture the worker's write before
  nsjail starts. Now the body is passed straight to 'sh -c <body> sh <args>'; no file
  is written into the rootfs at all.
- P1 (Codex): sandbox_registry_auth flowed through the generic setting loader which
  logs the value (raw auth.json credentials). Replaced with a secret-aware reload that
  loads directly and logs only a redacted 'configured=' message.

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

* fix(sandbox): redact sandbox_registry_auth in instance-settings write log too

The settings API also logs 'Set global setting <key> to <value>' via format_setting_value;
add sandbox_registry_auth to SENSITIVE_SETTINGS so the credential is redacted there as
well as on reload.

* fix(sandbox): don't silently disable cache eviction on podman images parse error

Re-review (cubic/Claude P2): serde_json::from_slice(...).unwrap_or_default() meant any
parse hiccup (e.g. podman omitting Size/Created via omitempty for a zero value, or
schema drift) silently degraded to an empty Vec and disabled eviction with no log.
Now Size/Created are #[serde(default)] (a missing omitempty key -> 0, not a whole-array
parse failure) and a real parse error warns + breaks instead of being swallowed.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:35:51 +00:00
hugocasa 00a96b82f3 add databricks icon (#9445)
Adds DatabricksIcon.svelte (brand mark, #FF3621) and registers it under
`databricks` in the shared APP_TO_ICON_COMPONENT map, so both the app and
hub frontends pick it up for the new Databricks hub integration.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-06-04 19:02:51 +00:00
hugocasa dad2bd0b93 add adobe acrobat sign icon (#9447)
Adds AdobeAcrobatSignIcon.svelte and registers `adobe_acrobat_sign` in
APP_TO_ICON_COMPONENT, for the Adobe Acrobat Sign hub integration
(windmill-labs/windmill-integrations#143).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:01:57 +00:00
hugocasa 93a74f229a oauth: add ServiceNow + make per-instance OAuth providers registry-driven (#9449)
* oauth: add ServiceNow provider; make per-instance OAuth registry-driven

ServiceNow's OAuth endpoints are per-instance
(https://<instance>.service-now.com/oauth_auth.do + /oauth_token.do), like
Snowflake's. Rather than add another bespoke special-case, generalize:
a registry entry may carry a `connect_config_template` (label/placeholder/
help_url + {instance}-templated auth_url/token_url + req_body_auth +
optional extra_params_key/strip_suffix). The instance-settings UI renders
one generic instance-name input for any such provider and substitutes
{instance} to build the per-client connect_config — a new per-instance
provider needs only a JSON entry, no frontend code.

- oauth_connect.json: servicenow + snowflake_oauth now carry a
  connect_config_template (snowflake keeps its account_identifier
  extra_params key for backward compatibility).
- windmill-oauth: add the ConnectConfigTemplate struct (frontend-only
  metadata; the backend's existing connect_config override resolves the
  concrete URLs generically — no other backend change).
- AuthSettings/InstanceSettings: replace the Snowflake + ServiceNow
  special-cases with one registry-driven path (instanceInputs map,
  setupTemplatedOauthUrls, loadInstanceInputs); per-instance providers are
  derived from the registry for the builtins list + dropdown.

Pairs with windmill-integrations#139 (ServiceNow hub integration).

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

* ci: point ee-repo-ref at servicenow-oauth EE branch (revert at merge)

Temporary CI pointer so check_ee_full / cargo_test build against the EE
slack-literal fix (windmill-ee-private#602). Revert to a pinned SHA once
that EE PR is merged.

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-06-04 18:58:47 +00:00
hugocasa eb55356018 add wiz icon (#9448)
Wiz star logomark (brand blue #0254EC) for the shared icon map
(APP_TO_ICON_COMPONENT), for windmill-integrations#144.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 18:58:29 +00:00
hugocasa f2f0812a04 feat(flows): opt-in to include the stopping step's result in early-stop errors (#9446)
* feat(flows): early stop can include the stopping step's result in the raised error

When a step uses Early Stop with "Raise an error message if stopped", the
flow result was entirely replaced with a static error object
({"error": {"name": "EarlyStopError", "message": "..."}}), discarding the
stopping step's own output. This made it impossible to stop+fail a flow
while preserving the data the step produced (e.g. an API that returns
HTTP 200 with a userErrors payload).

Add an opt-in `error_include_result` flag on StopAfterIf. When enabled on
the raise-error path, the raised payload becomes
{"error": {...}, "result": <step result>} instead of dropping the result.
Default is false, so existing behavior is unchanged. The option is threaded
through the worker's stop-after-if handling (including stop_after_all_iters_if
for loops/branchall) and exposed in the flow editor's Early Stop panel.

Fixes WIN-2012

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

* test(flows): cover early-stop error_include_result payload shaping

Add a regression test asserting that a step using Early Stop with a raised
error message and error_include_result=true fails the flow while preserving
the step output as {"error": {..}, "result": <step result>}, and that with
the flag off the result is the bare {"error": {..}} object.

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

* refactor(flows): nest early-stop step result inside the error object

Embed the stopping step's result under `error.result` rather than as a
top-level sibling of `error`. This keeps the flow result shape as
`{ "error": { .. } }` — identical to a normal error — so consumers that
key off the top-level shape (single `error` key) keep working, while the
data is still preserved for those that look inside the error object.

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

* feat(flows): always include the stopping step's result in early-stop errors

Drop the opt-in `error_include_result` gate. Since the step result is nested
inside the error object (`error.result`), the top-level result shape stays
`{ "error": .. }` — identical to a normal error — so consumers that detect or
parse failures by the top-level shape are unaffected. Gating it added schema
surface, plumbing, and a UI toggle for no real compatibility benefit.

Now, whenever a step early-stops with a raised error message, the flow fails
and the raised error embeds the stopping step's own result under
`error.result` (aggregated iteration results for loops/branchall). This
reverts the `StopAfterIf.error_include_result` field, its threading, the
OpenAPI/generated-client surface, and the editor toggle; the "Raise an error
message" tooltip now notes that the step result is included.

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

* feat(flows): gate early-stop result inclusion behind opt-in flag

Re-introduce the per-step `error_include_result` flag (default off) instead
of always embedding the step result. Although nesting the result under
`error.result` keeps the result *shape* backward-compatible, it does not
address data exposure: a failed flow's result is propagated to synchronous
webhook callers, the flow's failure module, and the workspace/global error
handler (commonly a Slack/email/outbound-webhook notifier). Always including
the step output would surface previously-redacted intermediate data to all of
those sinks for every existing error-stop flow.

Gating keeps the existing behavior (bare `{ "error": .. }`) as the default and
only embeds `error.result` when the flow author explicitly opts in, matching
the original issue's intent.

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

* fix(flows): omit error_include_result when false; refresh generated prompts

- Add `skip_serializing_if = "is_false"` to `StopAfterIf.error_include_result`
  so serialized flows are byte-identical when the flag is off. Fixes the
  `flowmodule_serde` round-trip test (cargo_test) and avoids churn on existing
  flows.
- Regenerate `system_prompts/auto-generated/` and `cli/src/guidance/skills.gen.ts`
  for the new OpenFlow `error_include_result` property. Fixes check-freshness.

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

* test(flows): cover error_include_result for the loop "stop after all iters" path

Add a regression test for the stop_after_all_iters_if branch, where `nresult`
already holds the aggregated iteration results — confirming `error.result`
carries each iteration's output (distinct from the per-step fallback path).

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-06-04 18:57:05 +00:00
Ruben Fiszel 24fa61d3c0 chore(main): release 1.717.1 (#9444)
* chore(main): release 1.717.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-04 10:20:28 +00:00
Ruben Fiszel 6b6c16e6bc chore(main): release 1.717.0 (#9439)
* chore(main): release 1.717.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-04 07:37:15 +00:00
centdix 819ba5e150 fix: read latest db draft for scripts/flows in global mode read tool (#9441)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 07:06:05 +00:00
hugocasa 468aa230e5 refactor: resolve workspace imports via /f/,/u/ not $f/,$u/ aliases (#9438)
* refactor: resolve workspace imports via /f/,/u/ not $f/,$u/ aliases

Keep the CLI managed tsconfig.wmill.json / `refresh tsconfig` / Deno
import-map QoL from #9378, but re-key it on the existing /f/,/u/ workspace
paths instead of the new $f/,$u/ specifiers. Verified /f/,/u/ resolves in
tsc, Bun, Deno, the in-app ATA editor, and the worker, so the $-prefixed
alias added no value. Drop the $f/,$u/ handling from the parser, dep-map,
deno_executor, bun loaders, ATA, relative_imports and monaco paths; revert
the windmill-parser-wasm-ts bump (1.714.0 -> 1.695.0). Also fold in the
cli/package-lock.json sync for the already-committed pg-gateway dependency.

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

* fix: drop duplicate relative-path check and restore rustfmt formatting

Follow-up cleanups to the previous commit's full-file reverts, which
restored pre-#9378 state that main had since improved:

- relative_imports.ts: remove the redundant duplicate d.startsWith('/')
  (pre-#9378 had it; #9378 had repurposed that line, so main has no dup).
- windmill-parser-ts/src/lib.rs: restore the multi-line new_source_file(...)
  formatting required by backend/rustfmt.toml (the single-line revert would
  fail `cargo fmt --check`). Now differs from main only by the $f//$u/ removal.

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-06-03 17:27:24 +00:00
Guilhem e4e0984e55 feat: let flow AI chat create and edit sticky notes (#9412)
* feat: let flow AI chat create and edit sticky notes

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

* docs: strengthen flow AI guidance to prefer groups for organizing flows

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

* fix: harden flow note validation (validate position/size, document color default and group acceptance)

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

* fix: make AI-created free notes draggable by seeding default position and size

Free notes need explicit geometry to be draggable/resizable in the editor; UI-created notes always set position+size but agent-created notes omitted both, so they couldn't be moved until resized. Seed defaults in validateFlowNotes.

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-06-03 17:27:01 +00:00
Ruben Fiszel d537c82c4f chore(main): release 1.716.0 (#9430)
* chore(main): release 1.716.0

* Apply automatic changes

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-03 14:01:02 +00:00
Guilhem 732dc0f617 apply COEP headers to dev static files for raw app editor (#9433) 2026-06-03 13:21:09 +00:00
centdix 79178f6f5a feat: use metadata model for small AI tasks (#9431) 2026-06-03 12:13:14 +00:00
hugocasa 220cd35cf7 feat: support $f/ and $u/ import path aliases for scripts (#9378)
* feat: support $f/ and $u/ import path aliases for scripts

$f/ and $u/ are local-friendly aliases for the absolute workspace
import paths /f/ and /u/. Unlike the /-prefixed form (which local tools
treat as a filesystem-root path), the $-prefixed form is a bare specifier
that can be remapped via tsconfig paths / Deno import maps, so the same
import resolves on the Windmill worker and in a local editor.

- worker: recognize $f//$u/ in the Deno import map and both Bun loaders
- dep-map/parser: normalize $f/->f/, $u/->u/ for lockgen + dep tracking
- cli: emit $f/$u path aliases in generated tsconfig.json / deno.json
- frontend: ATA + Monaco paths resolve $f//$u/ type hints in the editor

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

* feat(cli): split generated tsconfig into managed + user file with refresh command

Mirror the AGENTS.cli.md/AGENTS.md prompts model for the IDE tsconfig so the
recommended settings can evolve without ever clobbering user customizations:

- tsconfig.wmill.json: wmill-managed, always refreshed, holds recommended
  compilerOptions incl. the $f/$u path aliases (Deno: import_map.wmill.json)
- tsconfig.json: user-owned, created once, just extends the managed file;
  warn (never auto-edit) when an existing one doesn't reference it
- add 'wmill refresh tsconfig'; init generates it unconditionally (no longer
  gated behind resource-type namespace / a bound workspace)
- regenerate CLI guidance docs for the new subcommand

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

* fix(cli): address PR review on $f/ tsconfig generation

- handle existing deno.jsonc so we don't shadow it with a new deno.json
  (P1 identified by cubic)
- fix the bun-types hint that pointed users at the managed do-not-edit
  tsconfig.wmill.json; tell them to install + re-run 'wmill refresh tsconfig'
- document the .ts-extension-only local-resolution limitation (cross-flavor
  .bun.ts/.deno.ts/.fetch.ts scripts won't resolve in a local editor)

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

* feat(cli): warn when a project's tsconfig isn't wired to tsconfig.wmill.json

Mirror the prompts freshness check for the managed tsconfig so users with an
existing setup actually discover they're missing $f//$u/ resolution:

- embed a version hash in tsconfig.wmill.json (excludes the env-dependent
  bun-types 'types' entry so it doesn't false-positive)
- add warnIfTsconfigStale to the main.ts freshness hook, gated identically to
  the prompts check (skips init/refresh/help/version). When a tsconfig.json
  exists it warns one line (stderr) if the managed file is missing, not
  referenced via extends, or out of date; silent for non-TS projects.

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

* refactor(cli): make tsconfig setup equivalent to prompts (auto-wire + stale-only)

Unify the two managed-file systems so they behave identically:

- auto-wire an existing unlinked tsconfig.json/deno.json on init/refresh
  (add extends / importMap; merge into an array extends), instead of only
  warning. Parses JSON and falls back to a warning when it can't round-trip
  (JSONC comments, or a conflicting deno imports/importMap) — never corrupts.
- narrow warnIfTsconfigStale to stale-only, gated on the managed file
  existing, exactly like warnIfPromptsStale: it no longer nags about a
  missing or unlinked tsconfig.json, so a deliberately-custom/unlinked setup
  stays silent and a not-yet-initialized project isn't bothered.

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

* fix(cli): place tsconfig.wmill.json first in extends to preserve user base config

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

* feat(cli): migrate legacy tsconfig and require consent for custom configs

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

* refactor(cli): align prompts wiring to the same consent model

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

* chore(cli): bump windmill-parser-wasm-ts to 1.714.0 for $f/ $u/ aliases

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

* fix(worker): resolve $f/ and $u/ in deno lock generation

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

* test: narrow relative-imports lock-gen guard to deno import-map failure

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

* chore(cli): sync bun.lock with windmill-parser-wasm-ts 1.714.0

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

* fix(cli): warn when a custom tsconfig's paths would shadow $f/ $u/ aliases

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-06-03 11:58:12 +00:00
centdix 26b7270418 feat: auto-generate AI session names (#9399) 2026-06-03 10:35:03 +00:00
Guilhem c3d4c6474b gate session fork creation on CE workspace cap (#9411)
* feat: gate workspace fork creation in sessions behind enterprise license

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

* fix: gate session fork creation on CE workspace cap, not EE license

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:34:16 +00:00
centdix cf5fefb521 feat: add metadata generation model setting (#9418) 2026-06-03 10:33:16 +00:00
Ruben Fiszel 47c96204de chore(main): release 1.715.0 (#9421)
* chore(main): release 1.715.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-03 10:08:16 +00:00
centdix 11d1ad9a87 fix: omit temperature for gpt-5+ and o-series models on all providers (#9422)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:29:21 +00:00
Ruben Fiszel 3b2e748daf feat(frontend): add rebuild dependency map button to workspace settings (#9424)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:42:39 +00:00
Ruben Fiszel 89a7a37776 fix(backend): authorize single-job read endpoints by job/flow visibility (#9416)
* fix(backend): authorize single-job read endpoints by job/flow visibility

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

* feat(jobs): share read links + cached access checks for run visibility

- Cache the job read-access RLS probe (size-bounded LRU keyed by the caller's
  authz-relevant identity + job id; no TTL since job-side inputs are immutable).
- Inherit visibility along the full parent_job chain so any flow you can see lets
  you read its (deeply nested) steps.
- Share read links: GET /jobs/job_view_token/{id} mints a stateless
  HMAC(workspace_key, job_id) token (only if the caller can read the job); the
  token grants an authenticated member read of that job and its flow subtree via a
  ?view_token query param or X-View-Token header. Run page gains a Share button and
  honors a ?view_token link.
- Denied-but-existing reads now return 403 with guidance to request a share link
  (vs 404 for non-existent), and the run page renders that case with instructions.

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

* fix(jobs): address PR review — scope-tag check on mint, constant-time view-token verify

- P1 (Codex): get_job_view_token now enforces the caller's if_jobs:filter_tags
  scope before minting, so a tag-scoped token can't mint a transferable link for a
  job outside its tags. Adds a scoped-token regression test (allowed + denied).
- Constant-time view-token verification (HmacSha256::verify_slice) instead of
  comparing hex strings (Claude/Pi nit).
- get_completed_job_result: an authed reader passing an invalid suspended-secret
  triple now falls through to the normal visibility gate instead of erroring out
  (Claude nit); unauthenticated callers still rejected.
- Length-prefix the read-access cache key fields so no input values can collide.

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

* docs(api): add job_view_token to openapi spec; use generated client in run page

Addresses Codex review nit: the new GET /jobs/job_view_token/{id} endpoint was
missing from openapi.yaml (the source the frontend client is generated from). Adds
the path + operationId getJobViewToken, and switches the run page's Share button
from a raw fetch to JobService.getJobViewToken.

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

* fix(frontend): carry view_token on share-link downloads

Addresses Codex review: download actions bypass the request interceptor that adds
X-View-Token (downloadViaClient uses raw fetch; cookie-mode downloads use plain
hrefs), so a share-link viewer got 403 downloading logs/results/args. Append the
view_token query param to the job download paths (result/logs/args/flow-all-logs)
via a new appendViewToken() helper, covering both client-fetch and href modes.

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

* fix(jobs): enforce tag scope in require_job_read_access (view-token use side)

Addresses Codex P1: the view_token use-side bypassed if_jobs:filter_tags on
handlers that don't tag-filter their data query (result_by_id,
get_flow_job_debug_info, get_otel_traces) — a tag-scoped token could use someone
else's valid share token to read out-of-scope job data. Move the tag-scope check
into require_job_read_access (runs before any created_by/view_token/RLS grant), so
it applies uniformly to every gated handler; removes the now-redundant explicit
check in get_job_view_token. Adds a use-side regression test (scoped token + valid
out-of-scope view_token denied on otel/result_by_id; in-scope still allowed).

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

* fix(frontend): include workspace in share read link

Addresses Codex P1: the copied share URL omitted the workspace. The token is
signed with the run's workspace key and the logged layout only switches
$workspaceStore when the URL carries workspace=, so a recipient whose persisted
active workspace differs would open the link against the wrong workspace and the
token would fail validation. Pin workspace= alongside view_token in the link.

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

* fix(jobs): authorize get_result_maybe get_started branch for queued jobs

Addresses Codex P1: get_completed_job_result_maybe only gated when a completed row
existed; with ?get_started=true a non-reader reached the fallback branch and got
started:true for a running private job. Now fetches created_by and authorizes
(created_by/view_token/RLS, or anonymous for unauth) before disclosing
running-state; a non-existent job still returns started:false (leaks nothing).
Adds a regression test with a queued (no completed row) private job.

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-06-02 22:10:16 +00:00
Ruben Fiszel 00cd89fff3 chore(main): release 1.714.1 (#9408)
* chore(main): release 1.714.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-02 12:58:41 +00:00
Guilhem 2bff250f89 feat(frontend): harmonize diff button placement in script and raw app editors (#9410)
* feat(frontend): harmonize diff button placement across editors

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

* refactor(frontend): address review nits — drop unused diffDrawer param, fix stale comments

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-06-02 12:58:21 +00:00
Ruben Fiszel 2ac198396e chore(main): release 1.714.0 (#9390)
* chore(main): release 1.714.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-02 08:22:19 +00:00
Guilhem 30057445f9 avoid crypto.randomUUID in WorkspaceItemDrillPicker (#9405)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 07:16:55 +00:00
Guilhem de76668c10 fix(frontend): align Monaco editor font size with text-xs (#9161)
* fix(frontend): align Monaco editor font size with text-xs across viewports

* fix(frontend): make placeholder lineHeight reactive to fontSize

* fix(frontend): align GraphQL schema viewer font size with text-xs

The read-only GraphQL schema viewer was the lone Monaco instance still
inheriting Monaco's 14px default. Wire it through editorFontSize like
the other editors so it stays in sync with text-xs across viewports.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 23:29:19 +00:00
hugocasa ba0e4c8280 oauth: add salesforce provider (#9380)
* oauth: add salesforce provider

Register Salesforce OAuth (Authorization Code) for Windmill resource connect.
Production uses login.salesforce.com; the sandbox block points at
test.salesforce.com (URL overrides only; scopes inherited) per #9358, so a single
canonical `salesforce` resource type covers both with separate `salesforce_sandbox`
instance credentials.

Paired with the hub integration: windmill-labs/windmill-integrations#131.
The Salesforce icon already exists in the frontend (SalesforceIcon.svelte).

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

* Fix JSON syntax error in oauth_connect.json

* fix: add salesforce production tile to OAuth settings dropdown

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:49:33 +00:00
Guilhem 1275487f02 feat: refine ask-user-question chat display and keyboard nav (#9392)
* feat: refine ask-user-question chat display and keyboard nav

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

* style: use text-accent for ask-user-question icon

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

* fix: focus active choice when clicking ask-user-question card

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

* feat: disable chat input while an ask-user-question is pending

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

* refactor: focus active choice on card click instead of pointerdown

Preserves text selection on the question card; wired as a use: action so the non-interactive card needs no keyboard handler.

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

* refactor: extract isActiveUserQuestion shared predicate

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

* test: cover isActiveUserQuestion predicate

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-06-01 17:49:12 +00:00
centdix 943ef6eb20 feat: add workspace datatable tools to global AI chat mode (#9395)
* feat: add workspace datatable tools to global AI chat mode

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

* test: cover global-mode datatable tools pure logic

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

* feat: expose datatable SQL SDK reference via get_instructions in global mode

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

* feat: make datatable get_instructions language-aware, default TypeScript

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

* refactor: drop datatable/whitelist args from global init_app tool

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

* feat: flag missing datatable config as an explicit blocking error in global mode

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

* refactor: drop dead branch in exec_datatable_sql result handling

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-06-01 17:48:50 +00:00
centdix 5c20d6b4f7 feat: add global ai chat test tools (#9391)
* feat: add global ai chat test tools

* fix: avoid session id in flow test preview

* test: cover global flow preview ids

* test: require script and flow test tools

* fix: harden global flow test fallback

* Revert "fix: harden global flow test fallback"

This reverts commit 97254ef33a.

* fix: fallback from inactive flow test hook

* fix: list nested flow steps in errors
2026-06-01 15:43:07 +00:00
Guilhem 075faabf3b feat(frontend): surface local drafts in drawer editors with an unsaved-changes banner (#9335)
* feat(frontend): surface local drafts in drawer editors with an unsaved-changes banner

Drawer-based editors (the 11 trigger types, plus resource and variable)
restore unsaved edits from browser localStorage on open using the same
mechanism as flows/scripts, but only showed a transient "Reset to deployed"
toast with no way to review the diff.

Add a persistent "You have unsaved changes" banner below the drawer header
with Show diff / Discard actions, shown whenever the form diverges from the
deployed baseline. Replaces the toast for these editors; flows/scripts/apps
(full-page) keep their existing toast.

- new shared LocalDraftBanner.svelte (Alert-styled bar + DiffDrawer)
- DrawerContent: optional `banner` snippet rendered below the header
- useTriggerDraftSync: reactive `hasDraft`, `deployed`/`current` getters and
  `resetToDeployed`; drop the restore toast (banner supersedes it)
- wire the banner into all 11 trigger editors + variable; resource lifts its
  dirty state up to ResourceEditorDrawer via a callback + accessors
- fix ScheduleEditorInner.openNew not resetting initialConfig (reused editor
  instance kept a stale baseline, wrongly flagging a new schedule dirty)

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

* fix(frontend): address PR review on local-draft drawer banners

- LocalDraftBanner: snapshot diff sides at click time so the diff drawer
  doesn't keep updating as the user types behind it.
- VariableEditor / ResourceEditor: scope the banner and its Discard action
  to the selected workspace; the cross-workspace dirty state stays surfaced
  by the existing otherDirty Alert. Forward can_write via a new
  onCanWriteChange callback so the resource banner hides Discard in
  read-only mode (matching the trigger editors).
- useTriggerDraftSync: drop the now-unused path arg from maybeRestore and
  update all 11 trigger editor call sites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(frontend): deep-clone fallback in UserDraft.discard to avoid baseline aliasing

When a caller passed a live $state proxy as `fallback` (Variable/Resource
editors handed `initialStates[selected]` to the banner's Discard), the
handle's draft cell ended up sharing the same proxy as the caller's
baseline. Subsequent form edits mutated both sides in lock-step and the
dirty check kept reporting equal, so the banner never reappeared and
the Update button stayed disabled until the drawer was reopened.

Cloning the fallback inside `discard` (via `snapshotDraftValue`) gives
the handle a fresh tree and decouples the two reactive graphs. Trigger
editors already cloned at their call site (resetToDeployed); this just
makes the API self-contained for all callers.

Also switch the variable form's "Audit log for each access" alert from
warning to info — it's informational, not a warning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(frontend): honor disabled prop in LocalDraftBanner's diff drawer

The banner's `disabled` prop hid the inline Discard button but the diff
drawer's "Discard changes" action was still wired unconditionally, so a
read-only user could bypass the hidden inline action via Show diff.
Gate the diff-drawer button on the same flag so both surfaces agree.

Flagged by cubic and Codex on PR #9335.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:46:30 +00:00
Guilhem e4213c1ab8 feat(flow-ai): constrain flow-group colors to the NoteColor palette (#9343)
The flow AI chat's set_flow_json tool lets the model set a `color` on each
semantic flow group, but nothing told it which colors are valid, so it would
sometimes emit hex codes / arbitrary CSS color names. Those render with
default styling at best and break the group color picker at worst.

- core.ts: the set_flow_json schema `.describe()` and the `groups` system-prompt
  bullet now spell out that `color` MUST be one of the palette names
  (yellow, blue, green, purple, pink, orange, red, cyan, lime, gray) — no hex,
  no CSS colors — and that omitting it lets the editor auto-assign one.
- helperUtils.ts: validateFlowGroups now rejects any color outside that palette,
  sourced from the NoteColor enum so the two can't drift.
- helperUtils.test.ts: tests for reject-unknown / accept-known / accept-omitted.

Split out of the sessions branch (gl/layout-ai), where it had been bundled
into the large feature commit; it's an independent flow-AI improvement.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 11:15:22 +00:00
Guilhem eadeac248b feat: sessions page with isolated AI chat + flow editor (#9034)
* feat(sessions): chat + editor side-by-side with multi-session state

Introduces the Sessions feature: a workspace where the AI chat and an
editor (flow / script / app / raw-app) sit side-by-side, with each session
having its own AIChatManager instance, history, and target item. Sessions
are persisted across reloads and can be staged into forks for review.

Key pieces:

- sessions/ — SessionWrapper (the split-pane shell), SessionPicker
  (sidebar list), SessionForkBar, SessionWorkspaceBar, FlowEditorView /
  ScriptEditorView / AppEditorView / RawAppEditorView, ForkDiffDrawer,
  sessionRuntime (per-session AIChatManager + draft state),
  sessionState (in-memory + persisted index), sessionUnread, sessionScope,
  appDraftCodec / flowDraftCodec, forkEditUrl, /sessions route.

- WorkspaceItemDrillPicker refactor — extracts WorkspaceItemRow + adds
  surfaceAI drafts, stale-while-revalidate. workspacePicker.ts drops
  explicit invalidate() in favor of always re-fetching in the background.

- ForkDiffDrawer + WorkspaceItemDiffViewer — per-kind diff bodies
  reusable from the compare page. FlowGraphDiffViewer / FlowGraphV2 gain
  inlineDiff forwarding + onHeight callback for equal-height layout.

- Global AI chat sessions plumbing — AIChatManager exports the class +
  adds disabledModes, beforeSend hook, scoped instance context. AIChat /
  AIChatDisplay accept session-only props (wideLayout, emptyHint,
  inputPreface, hideHeader, hideModeSelector, forceDisabled). Chat
  preserved across /flows/add → /flows/edit, /scripts/add → /scripts/edit.

- Draft-first loaders — sessions open drafts when present, otherwise
  seed a draft from the last deployed value via globalDraftStore.
  RawAppEditor / AppEditor / AppEditorHeaderDeploy get newApp prop +
  fixes so draft-only apps can deploy.

- Compare page (/forks/compare) — bigger overhaul to plug into the new
  drawer.

- Sidebar — Sessions entry + unread badge + status dot in
  SidebarContent / MenuButton / SideBarNotification.

- Misc fixes — chat group color palette constraint, deploy_workspace_item
  confirmation dropped, open_preview tool, picker drafts surfacing,
  fork archive/delete buttons on compare page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): bypass UserDraft inside session panes + sessionUnread crash

After merging main's UserDraft PR (#9121) into the sessions branch, two
integration issues surfaced:

1. AppEditor.svelte calls `UserDraft.use<App>('app', path)` at the
   component level — keyed by ($workspaceStore, 'app', path). Sessions
   that haven't materialized a fork yet stay at the user's main
   workspace, so a session targeting an app at the same path as a
   regular /apps/edit tab shared the same LS key. The session would
   read the regular tab's autosave and write its fork-edits back over
   it.

   Gate UserDraft.use on `!getContext('aiChatManager')` — sessions
   inject the manager via setContext, so inside a session pane the
   handle is `undefined`, stateApp falls through to the `app` prop
   the session loaded, and the auto-save $effect bails. Same gate on
   the four UserDraft.remove call sites in AppEditorHeader and
   RawAppEditorHeader so save/deploy from a session pane doesn't wipe
   the LS draft of a non-session tab at the same path.

2. sessionUnread.svelte.ts called useLocalStorageValue at module
   scope. Main's PR added a deep-mutation $effect inside that helper,
   which now requires component-initialization context — every page
   crashed at import time with `Svelte error: effect_orphan`.
   Replaced with a plain module-level $state + manual localStorage
   persist; same reactivity contract for callers.

3. ScriptEditorView.svelte was passing a `replaceStateFn` prop that
   ScriptBuilder dropped on main. Removed.

Verified end-to-end with Playwright:
- /flows/edit/{path} regression: UserDraft handle still created, no
  console errors
- /sessions loads, sessionUnread doesn't crash
- Session targeting non-raw app `u/admin/userdraft_collision_test`
  displays the fork content (FORK_ONLY_MARKER) even with an LS
  poison at `userdraft/w/local/app/{path}` containing a
  POISONED_BY_REGULAR_TAB_AUTOSAVE marker; poison remains untouched
  after the session loads and renders

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): stop fork-create retry loop on first user message

Removed the SessionWrapper $effect that retroactively committed the
session's workspace from the in-memory chat history. When opening a
session whose previous commit attempt had failed (or whose response was
lost) the effect ran in a tight retry loop, flooding the user with
`workspace_pkey` violations from `create_workspace_fork`.

The send path already commits through `AIChatManager.beforeSend` →
`commitSessionWorkspace`, which is the deterministic moment-of-action.
The $effect was a redundant reactive bridge that turned every backend
failure into an infinite retry.

Also hardens `materializeFork`/`commitSessionWorkspace` so the most
common cause of the duplicate-key error self-heals:

- `materializeFork` short-circuits when `fork.id` is already in
  `$userWorkspaces` (the previous create actually succeeded, we just
  lost the response). On a `workspace_pkey` catch, refresh the workspace
  list and adopt the existing row instead of toasting an error.
- On a real `materializeFork` failure, `commitSessionWorkspace` now
  drops `pending_fork` so the session falls through to the
  workspace-pick fallback instead of looping on the same broken intent.

* feat(sessions): show EditorHeader breadcrumb in the not-found state

When a session's target item has been deleted or moved, the editor pane
used to render a bare "Script not found at path X" line — leaving the
user with no way to navigate to a different target without backing out
of the session.

Each editor view now renders a `SessionItemNotFound` shell instead: a
real `EditorHeader` (read-only summary, no pen popover) with a
breadcrumb keyed to the missing kind+path, plus the "not found" copy
below. Clicking any breadcrumb segment opens the workspace picker
scoped to that level — pick a replacement and the session swaps target
via the existing `onNavigate` callback.

`SessionItemNotFound` maps `raw_app` to `EditorHeader`'s `kind: 'app'
+ raw_app: true` so the picker routes through `/apps_raw/...`; the
local label still says "Raw app not found" (not "App not found") so
the user knows which surface is missing.

* fix(picker): stop self-feeding fetch effect that OOM'd the tab

The drill picker's $effect watched `scope` and called `ensureLoaded`
on every change. `ensureLoaded` reads `loaded[kind]` synchronously
(to decide whether to show a spinner), so the effect ended up
subscribed to the very signal it fills. Each fetch result wrote
`loaded[kind] = items`; Svelte 5's $state proxy notifies on every
property set even when the reference is unchanged from cache, which
refired the effect, which called `ensureLoaded` again, which awaited
the cached fetch, which wrote `loaded[kind]` again... runaway loop.

In `/scripts/edit/...` the picker's lifecycle stabilised quickly
enough to mask the loop, but in a session pane (multiple warm
sessions, picker kept alive by the surrounding state) the cycle
spun freely — 29.8 million iterations in <100 ms during testing,
enough to OOM Firefox / kill the Chromium tab.

Two changes:

- Replace the scope-watching $effect with an explicit `setScope()`
  helper called from `drill()`, `goUp()`, and `onMount`. Fetch is
  now a callback reaction to user navigation, never a reactive
  consequence of one. No closed feedback cycle is possible.

- Untrack the `loaded[kind]` read inside `ensureLoaded`. The search
  $effect (which loads every kind on first keystroke) is still a
  reactive caller; the untrack stops it from subscribing to the
  signal `ensureLoaded` fills, so the same loop can't form there.

* feat(script-editor): wire initialTestPanelCollapsed through ScriptBuilder

The `initialTestPanelCollapsed` prop was already declared on
`ScriptBuilderProps` (used by the session preview to start the editor
with the run/test pane closed) but never destructured in
`ScriptBuilder.svelte`, so the value silently dropped on the floor
and the test pane always opened.

- `ScriptBuilder.svelte` — destructure the prop and forward it to
  `<ScriptEditor>`.
- `ScriptEditor.svelte` — accept the prop and seed `rawTestPanelSize`
  to 0 when true, while keeping `storedTestPanelSize` at the default
  30 so the user's first toggle expands the pane to a sensible width
  rather than 0.

Regular `/scripts/edit/...` doesn't pass the prop → default `false`
→ panel still opens by default.

* fix(sessions): resolve aiChatManager via context in AskUserQuestionDisplay

Inside a session the chat uses a per-pane AIChatManager injected via context. AskUserQuestionDisplay imported the global singleton, so answers clicked in a session dispatched to the singleton's callback map and the AI loop stalled. Resolve via getContext with singleton fallback, matching ChatMode / ToolExecutionDisplay.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(raw_apps): let preview start in single-view on the preview tab

Add a defaultSplitWithPreview prop (default true). When false (session preview), the editor boots in single view with the preview tab selected: gate the onMount default-file activation, the setActiveDocument auto-activation, and iframeShouldMount so the UI Builder bundler iframe still mounts when preview is the active tab. RawAppEditorView passes defaultSplitWithPreview={false}.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(copilot): add get_preview_status tool and make open_preview idempotent

So the assistant can tell whether the session preview already shows the item it just edited, instead of re-opening or re-offering it. Mirrors the open_preview handler plumbing (setGetPreviewStatusHandler) and the session runtime registers it alongside open_preview. open_preview now returns 'already open' when the requested target matches the active session's current target. The system prompt steers the AI to check status before offering. Unit tests cover the no-arg schema, the session-only error, and handler dispatch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): make script preview reactive to AI draft writes

ScriptEditorView read the draft via static UserDraft.get inside an effect, which only subscribes to UserDraft's reactive cell when a live entry exists. None did for the preview path, so the chat's writes (UserDraft.save) only touched localStorage and the open preview never updated. Hold a live handle via UserDraft.useMany (reactive getter so it re-acquires when open_preview swaps the path without remounting) and read inbound through handle.draft, materializing the shared $state cell that bridges the chat's writes to the editor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): make raw-app preview reactive to AI draft writes

Mirror of the script-preview fix. RawAppEditorView read the draft via static UserDraft.get inside an effect, which only subscribes to UserDraft's reactive cell when a live entry exists. None did for the preview path, so the chat's raw-app writes (UserDraft.save / setDraftAndMeta, from write_app_file / patch_app_file / write_app_runnable) only touched localStorage and the open preview never updated. Hold a live handle via UserDraft.useMany (reactive getter so it re-acquires when open_preview swaps the path without remounting) and read inbound through handle.draft. Verified in-browser: an external UserDraft.save live-updates the bound summary in the open preview.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): make flow preview reactive to AI draft writes

Mirror of the script/raw-app preview fixes, completing two-way binding for all three session editor kinds. FlowEditorView read the draft via static UserDraft.get inside an effect, which only subscribes to UserDraft's reactive cell when a live entry exists — none did, so the chat's writes (write_flow / patch_flow_json / set_flow_module_code) only touched localStorage and the open preview never updated. Hold a live handle via UserDraft.useMany (reactive getter so it re-acquires when open_preview swaps the path without remounting) and read inbound through handle.draft. Verified in-browser both directions: an external UserDraft.save live-updates the flow header summary and rebuilds the module graph; a preview edit propagates through the debounced save to both UserDraft.get and the chat's getGlobalDraft adapter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(sessions): surface local-storage drafts in fork diff & compare page

Augments the backend fork-vs-parent comparison with browser-local (UserDraft) drafts so a session's uncommitted AI/user changes are visible in the Fork Diff Viewer and the /forks/compare page. Adds forkDraftDiff.ts (augmentForkComparisonWithLocalDrafts + getForkItemValue), a 'local changes detected' / new-draft warning surface (checkbox-slot warning icon, no-op-baseline filtering, dedup), a 'Local draft <> fork' tab in DiffDrawer, and selectTooltip/nonSelectableTooltip plumbing in Row/WorkspaceDeployLayout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Revert "feat(sessions): surface local-storage drafts in fork diff & compare page"

This reverts commit 3cfd858e36.

* fix(sessions): leave for home when switching workspace from the session page

An AI session is scoped to its (forked) workspace, so it makes no sense to keep showing it after the user picks a different workspace. The workspace switcher's link href now points home on the session route (the link navigation wins over onClick's preventDefault), and toggleSwitchWorkspace also redirects home there as a fallback. Session-switching uses a separate path (syncWorkspaceTo), so it's unaffected — which is why reacting at the switcher is more robust than watching workspaceStore.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): clear session highlight off the session page; default delete-fork on

Two SessionPicker fixes: (1) only highlight the active session while on the /sessions route — currentSessionId lingers after navigating away, so the row stayed selected in the sidebar; gate the highlight on the route. (2) The 'Also delete forked workspace' toggle in the delete-session modal now defaults to on (the fork is tied to the session and would be orphaned otherwise); resets keep it defaulted-on for the next open. User can still untick it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(copilot): say "local storage" instead of "draft" in write-tool status

The global chat's write tools persist to the browser's localStorage (UserDraft), not a workspace draft. The tool status / result messages now say the item was saved to local storage (and discard says it was discarded from local storage) so users aren't misled into thinking a workspace draft was created. Covers the shared script/flow/trigger/resource/variable helpers and the app tools.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): sidebar collapse, new-session chat, fork delete & not-found nits

- Hide the collapse chevron and make the section header non-interactive when there are no sessions; reset the persisted collapsed state while the list is empty so the first session always appears expanded.
- Stop grafting a recent past chat onto a freshly created session: ensureChatIdsSeeded now skips transient sessions, so the seed only pairs untagged chats with pre-existing sessions.
- After deleting a fork from a session (SessionPicker / SessionWrapper), fall back to the fork's parent workspace when the deleted fork was the active one, instead of stranding the user on a deleted workspace.
- Show a 'Session not found' message (with a New session action) when the URL names a session that doesn't exist, rather than rendering a blank page.

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

* feat(copilot): expose preview tools only to session chats

open_preview and get_preview_status drive a session's side-panel editor, so they only make sense inside an AI session. They were always present in the global tool list and just errored when called outside a session. Now AIChatManager carries an isSessionChat flag (set by sessionRuntime.createRuntime); the GLOBAL-mode branch uses globalToolsFor({ sessionPreview }) to drop the two tools for the regular side-panel chat, and prepareGlobalSystemMessage omits their guidance unless previewTools is set. The module-level handlers + in-tool error guards stay as defense in depth.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(flow-editor): move intra-editor chat preservation to its own PR

The beforeNavigate / preserveChatOnDestroy guard that keeps the global FLOW
chat alive across same-flow editor remounts is a standalone global-chat fix,
unrelated to sessions. Split out to #9339; FlowEditor reverts to the plain
session-guarded saveAndClear lifecycle here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(raw-app): pre-boot session editor hidden so files open instantly

In single-view (sessions) the UI Builder iframe was mounted inside a
display:none wrapper while the Preview tab was active, so the VS Code
workbench booted at 0x0, threw in its LayoutService ("Unable to figure
out browser width and height"), and wedged the editor on "Loading
editor" with no recovery when later revealed.

Keep the iframe mounted at the editor area's real width and hide it with
visibility instead of collapsing it: Monaco boots correctly while hidden,
and revealing a file is an instant un-hide (no reload, no relayout, no
latency).

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

* chore(flow-ai): move flow-group color-palette work to its own PR

The flow-group color-palette guidance + validateFlowGroups guard + tests are
an independent flow-AI improvement, not part of sessions. Split out to #9343;
these three flow files revert to their main state here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): hide the in-editor Flow AI Chat button in the session preview

The flow preview pane in a session already sits next to the session's own AI
chat, so FlowBuilder's in-editor "Flow AI Chat" toggle (which opens the global
singleton chat) is redundant and confusing there. Pass
customUi={{ topBar: { aiBuilder: false } }} from FlowEditorView, reusing the
existing showFlowAiButton gate (!disableAi && customUi?.topBar?.aiBuilder !=
false) that flows down to FlowStickyNode — no new prop needed.

Verified in-browser: the button (WandSparkles) renders in the regular
/flows/edit route but is absent in the session preview for the same flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): mirror /scripts/add for never-saved scripts in editor preview

An AI-created script with no backend version yet left savedScript undefined
in the session preview, which disabled Save draft and hid Show diff. Open it
as a new script (empty initialPath) like /scripts/add so Save draft is enabled
and creates it on first save; seed the path as already-chosen
(initialPathChosen) so the summary->path auto-slug does not rename the
AI-assigned path. On first save ScriptBuilder writes savedScript back through
the bind and flips into edit mode (Save draft + Show diff) without navigating
away.

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

* fix(sessions): refresh fork diff count after an editor draft save

The fork-bar diff count reads a cached comparison refreshed only on AI-turn-end or tab refocus. A 'Save draft' in the session editor registers in the backend fork tally asynchronously (~300ms after the create returns), so the count stayed stale until one of those triggers fired. Add SessionRuntime.scheduleForkComparisonRefresh() (re-fetches at 700ms + 2200ms to clear the async tally) and wire it to onSaveDraft in ScriptEditorView and FlowEditorView.

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

* fix(sessions): don't auto-open the settings drawer in script preview

When the AI's open_preview tool previews a never-saved script, ScriptEditorView
passes initialPath='' so ScriptBuilder behaves like /scripts/add. That empty
path also triggered ScriptBuilder's auto-open of the settings drawer, which is
unwanted in the session preview where the AI manages metadata. Pass
neverShowMeta so the drawer stays closed on mount; the Settings button still
opens it manually.

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

* fix(sessions): don't host legacy drag-and-drop apps in the editor preview

The session preview pane only hosts code-based items (flow, script, raw
app). Drop the legacy 'app' kind from SessionTarget and the open_preview
tool, and route a legacy app picked in the drill picker to the standalone
/apps/edit editor instead. Removes the now-dead AppEditorView and its
runtime load path.

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

* fix(sessions): don't prompt to discard raw-app changes on navigation

In a session the raw-app editor's content is continuously persisted to the
UserDraft (localStorage), so tearing the editor down on navigation loses
nothing. Skip the UnsavedConfirmationModal (and its beforeNavigate guard)
when the editor is mounted inside a session pane; the standalone /apps_raw
editor still shows it.

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

* fix(sidebar): pin Help to the bottom instead of floating

The bottom of the sidebar stacked the User/Settings cluster and the Help
block with a fixed ~40px gap between them, plus a bottom margin that kept
Help from sitting flush — so Help appeared to float. Drop those fixed
margins so the cluster and Help stay glued at the bottom with a small gap
and Help is flush, and let mt-auto own the flexible space above the group.
Add pt-4 so the cluster keeps a minimum gap from the Triggers section when
the sidebar runs out of room and that flexible space collapses.

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

* fix(sessions): surface diff/discard for AI script drafts in preview, refresh diff on deploy

loadScript built the editor's scriptStore by aliasing and mutating savedScript.val, so the deployed baseline got overwritten with the draft content and the diff compared draft-vs-draft. Clone the baseline before layering the AI draft on top. On load, when the local draft diverges from the saved baseline, surface a toast ('AI saved a local draft') with Show diff (opens the diff drawer with a Discard-draft button) and Discard local draft — mirroring the regular /scripts/edit affordance the session's parallel loader omitted. Also wire onDeploy (alongside onSaveDraft) to scheduleForkComparisonRefresh so the fork diff count refreshes after a deploy, not just on an AI turn or tab refocus.

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

* fix(sessions): script preview restore/deploy feedback; drop on-load draft toast

- Implement real restoreDeployed/restoreDraft for the diff drawer: the shared loadScript-based handler was a no-op (loadScript early-returns on the loaded path and would re-read the local draft). Reset the live UserDraft handle to the chosen baseline (deleting the backend draft for 'restore to deployed') so the inbound effect syncs the editor.
- Show a 'Deployed' toast on deploy: the default Deploy takes ScriptBuilder's no-toast branch (the editor navigates away instead); the session stays put, so surface the success toast.
- Remove the on-load 'AI saved a local draft' toast: unnecessary in a session, where the user already expects their changes to be present. Diff/discard remain reachable via ScriptBuilder's Show diff + the diff drawer's restore buttons.

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

* fix(sessions): gate breadcrumb picker draft-merge behind the dev flag

The WorkspaceItemDrillPicker merges localStorage UserDrafts into its
navigable items so in-flight session/chat drafts are reachable. That
merge was ungated, so with the sessions dev flag off it also surfaced
the standalone editors' autosave drafts — they appeared as navigable
rows that 404 on the backend draft fetch. Gate aiDraftsForKind on
isGlobalAiEnabled() so it is a no-op without the flag (no sessions
exist then anyway); inside sessions the merge still works.

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

* fix(editor): reload script/flow editor on client-side breadcrumb nav

Picking a different item in the editor-header breadcrumb picker calls
goto() for a client-side navigation. SvelteKit reuses the same +page
instance across a path-param change, but the script and flow editor
routes captured `draftPath` and the `UserDraft.use()` handle once at
mount and never remounted ScriptBuilder/FlowBuilder. The URL and title
updated while the editor kept showing the previous item's breadcrumb,
summary and content; only a full reload showed the navigated-to item.

Mirror the pattern the app / raw-app editors already use:
- Derive the draft path from the URL and key the handle off it via
  `UserDraft.useMany` (a stable proxy onto the current handle), so the
  reload reads/writes the navigated-to item's draft instead of the
  previous one's — fixing the stale draft-comparison too.
- Gate the builder subtree on a `renderEditor` flag flipped false when a
  navigation kicks off the reload and true once the data is ready, so
  the builder cleanly unmounts and remounts once against stable data. A
  synchronous `{#key}` swap instead races Monaco's async init against
  the torn-down container.
- Flows also reset `nobackenddraft` per navigation so a fresh load
  reconsiders the backend draft.

The unsaved-changes guard is unaffected (it runs in beforeNavigate,
before the remount). The app and raw-app editors already handled this
and are unchanged.

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

* feat(sessions): sync preview with the deployed version on editor + chat deploy

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

* feat(sessions): reload the preview after a chat raw-app deploy

The deploy-reload-preview callback added previously only fired for script and
flow. Now that the merged deploy_workspace_item tool can deploy raw apps
(bundle + createAppRaw/updateAppRaw), wire raw apps in too. A raw app deploys
under type 'app' but the session preview addresses it as 'raw_app', so the
deploy handler maps 'app' -> 'raw_app'; the runtime open-check gains the
loadedRawAppPath case. syncPreviewWithDeployed already handled 'raw_app'
(discard the local draft + force-reload via loadRawApp), so no runtime change
was needed there.

Adds a unit test asserting deploy_workspace_item(type:'app') notifies the
session handler with { kind: 'raw_app', path }.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): address Claude PR review (3 P1 + 3 P2 + test)

P1:
- Drop the hardcoded placeholder default sessions (u/guilhempw/...). New users
  (empty/cleared/private-browsing localStorage) now start with no sessions and
  see the empty state instead of unresolvable "session not found" rows.
- Scope the preview/deploy tool handlers to the *calling* session. open_preview,
  get_preview_status and the deploy reload handler dispatched via the global
  currentSessionId, so a backgrounded session's tool call mutated the UI-active
  session. The calling session id is now carried in the per-manager tool
  `helpers` (AIChatManager.sessionId, set in createRuntime) and threaded through
  the tool ctx to the handlers, which dispatch to it (falling back to the active
  id only when absent). Keeps backgrounded sessions isolated.
- beforeSend now aborts the send on failure: commitSessionWorkspace throwing used
  to be swallowed, letting the message go out against the wrong workspace
  silently. Now it toasts and returns. Also guarded the unguarded
  listUserWorkspaces refresh in materializeFork's duplicate-key self-heal so a
  second network failure can't rethrow past the toast-and-return contract.

P2:
- disposeRuntime now clears the fork-comparison refresh timers (700ms/2200ms)
  via a new runtime.dispose(), so an evicted/deleted runtime can't fire a stray
  refreshForkComparisonNow/compareWorkspaces after teardown.
- Convert Svelte 4 on:click -> Svelte 5 onclick on the Button components in
  SessionWrapper, SessionForkBar, ForkDiffDrawer, SessionPicker, sessions/+page.
- WorkspaceItemRow's <a href> branch gains role="option" + aria-selected to match
  the <button> branch, for consistent listbox semantics.

Tests:
- core.test.ts: deploy_workspace_item(type:'app') threads the calling session id
  through helpers to the deploy handler ({ sessionId, kind:'raw_app', path }).
- New sessionState.test.ts unit-tests deriveForkStatus + isForkSession across
  all branches (root/fork/unavailable/draft, ahead/behind/diverged/in_sync).

svelte-check 0 errors; 57 frontend unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(copilot): collapse deploy preview-reload dispatch to a type→kind map

Replace the if/else-if that mapped deploy type to preview kind with a single Partial<Record<WorkspaceItemType, ...>> lookup + one if. Non-previewable types map to undefined → no dispatch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(copilot): use getAiChatManager() instead of inlining the context fallback

Six chat components still inlined
`getContext<AIChatManager>('aiChatManager') ?? singletonAiChatManager` even
though aiChatManagerContext.ts already exports getAiChatManager() for exactly
this (the resolve-scoped-instance-or-fall-back-to-singleton pattern, already
used by AIChatDisplay/AIChatInput/AIChatMessage/CodeDisplay). Adopt it in
DatatableCreationPolicy, ChatMode, ToolExecutionDisplay, AIChat,
AskUserQuestionDisplay and flow/FlowAIChat, and drop the now-unused getContext /
AIChatManager / singletonAiChatManager imports (FlowAIChat keeps getContext for
its FlowEditorContext/FlowCopilotContext lookups).

No behavior change — getAiChatManager() is the same resolution.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): consistent script deploy → preview sync; trim session deploy menu

Two related session deploy fixes + clarifying comments.

1. Hide the extra deploy-dropdown options in the session script preview. The
   editor always "stays" and is already scoped to a fork, so Deploy & Stay here,
   Fork, Edit in workspace fork, Exit & See details and Export as YAML/JSON make
   no sense there — only "Show diff" is kept. ScriptBuilder gains
   `inSessionPane = !!getContext('aiChatManager')` (same pattern ScriptEditor
   uses) and gates those items. (They were correctly absent for never-deployed
   session scripts but leaked for deployed ones.)

2. Fire onDeploy on every successful script deploy. ScriptBuilder previously
   skipped onDeploy for "Deploy & Stay here" and lib scripts (it just re-pinned
   parent_hash + toasted), so a session preview wouldn't sync after those. Now
   onDeploy always fires with a `stay` flag; route consumers skip navigation when
   stay (behaviour identical to before — stay → toast only, primary → navigate),
   and the session ignores stay and always syncs. With (1) hiding Deploy & Stay,
   this now covers the lib-script-in-session case.

3. Comments: RawAppEditorHeader / AppEditorHeader note that the
   `if (!inSessionPane) UserDraft.remove` guards are intentional — the editor
   doesn't own the localStorage draft in a session (the runtime does, keyed by
   the fork); the session-side equivalent is the View's onDeploy →
   runtime.syncPreviewWithDeployed (discard fork draft + reload to deployed).

svelte-check 0 errors; session dropdown verified to show only "Show diff" for a
deployed script, route deploy menu unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sidebar): single Menubar so bottom menus hover-switch (WIN-1993)

The bottom sidebar group split Settings/Workers/Folders/Logs and Help across
two separate <Menubar> components. melt-ui's hover-to-switch (open menu closes
when another trigger in the same Menubar is hovered) only coordinates within a
single Menubar, so hovering between the two groups left both menus open
(stacked) instead of switching. Collapse them into one Menubar, wrapping each
group in its own flex container to preserve spacing.

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

* refactor(editor): gate external code sync behind opt-in syncExternalCode prop

The unconditional `code` prop->Monaco sync effect added for sessions
live-preview ran for every <Editor> caller (14 call sites). Most either
bind:code with their own external-sync (e.g. ScriptEditor) or treat code as
init-only, so a blanket setValue risked clobbering them. Gate the effect on a
new opt-in `syncExternalCode` prop (default off) and enable it only at the two
flow inline-rawscript editors — the case that actually needs external updates
(AI chat editing a flow module's content reflecting live in the preview).

Verified in-browser: AI-driven external edit to a flow step now reflects live
in Monaco, and typing keeps the caret intact (round-trip guard).

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

* fix(sessions): address P1 review findings (commit-abort, render-stuck, workspace sync)

From the cubic/Claude PR review:

1. beforeSend now aborts the send when the workspace isn't committed. The earlier
   fix only caught a *thrown* error, but commitSessionWorkspace returns undefined
   (never throws) when a staged fork fails to materialise — so the first message
   + its tool calls shipped to get(workspaceStore) (the parent). beforeSend now
   throws on undefined so AIChatManager's catch toasts + aborts.

2. The script/flow edit reload effect set renderEditor=false then called
   loadScript()/loadFlow(); a rejected fetch left renderEditor stuck false, so the
   editor pane vanished and never remounted. Both calls now .catch → toast +
   renderEditor=true (token-safe), so the pane always remounts.

3. SessionWrapper.moveAndActivate now syncWorkspaceTo(target) — moving a session
   off an unavailable workspace was leaving the app pointed at the old one
   (mismatch with moveSessionToNewFork / handleConfirmedDelete).

Test: sessionState.test.ts pins commitSessionWorkspace's failure contract
(returns undefined + drops pending_fork when the fork fails) — the invariant the
beforeSend abort relies on. svelte-check 0 errors; 58 frontend unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): address P2 review findings (cubic)

Draft round-trip:
- appDraftCodec: carry custom_path through runtimeRawAppToDraft /
  applyDraftToRuntimeRawApp (+ seed it in loadRawApp) so a session round-trip
  no longer erases a raw-app draft's custom URL.
- sessionRuntime.loadScript "no draft" path: structuredClone the baseline before
  setting parent_hash — it could alias `result` (= savedScript.val) and corrupt
  the pristine deployed baseline the diff drawer reads.
- FlowEditorView: include `summary` in the inbound/outbound dedup sigs so
  summary-only changes propagate/persist.

Workspace-state on navigation:
- SidebarContent (post-delete) and workspace_settings (post-archive): guard the
  listUserWorkspaces() refresh so a transient failure can't strand the user on
  the just-removed workspace, and refresh the list before switching to parent.
- WorkspaceMenu: keep ?workspace=<id> in the session-page workspace href so a
  modifier/middle click (which bypasses onClick) lands in the right workspace.

UI/keyboard:
- WorkspaceItemRow: indent adds to the px-3 base (calc) instead of replacing it.
- ForkDiffDrawer: ArrowLeft maps a 2-segment file path (f/foo) to its scope
  folder (folder:f/foo) instead of a nonexistent folder:f.
- flows/edit: defer flowBuilder setup (primary schedule, draft triggers,
  loadFlowState) until after the builder remounts (renderEditor=true + tick),
  so reload-time state restoration isn't skipped on the unmounted builder.

svelte-check 0 errors; 58 frontend unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(sessions): unit-test the P1/P2 review fixes (extract pure helpers)

Extract the pure logic touched by the review fixes into small tested helpers
(behaviour-preserving) and add unit tests:

- appDraftCodec.test.ts — custom_path survives the runtime↔draft round-trip (A1).
- forkDiffNav.ts/.test.ts — parentFolderKey (extracted from ForkDiffDrawer):
  ArrowLeft parent resolution incl. the 2-segment-path case (C2).
- workspaceMenuHref.ts/.test.ts — extracted from WorkspaceMenu: session-route
  href keeps ?workspace=<id>; off-session swaps the param (B2).
- flowDraftSig.ts/.test.ts — extracted from FlowEditorView (dedups 3 sig sites):
  the dedup signature includes summary, so summary-only changes propagate (A3).

(commitSessionWorkspace failure-contract test for the beforeSend P1 landed with
the P1 commit.) svelte-check 0 errors; 75 frontend unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sessions): address second-round review (Pi + Codex)

Three findings flagged post-push (cubic was fully addressed in the prior
commits; this commit covers the new ones):

- [P1] commitSessionWorkspace non-fork branch — when a session created
  inside a fork defaults pending_workspace_id to the family root, commit
  set s.workspace_id but never synced workspaceStore. First send's
  logAiChat + tool calls then ran against the wrong (still-fork)
  workspace. Fix: syncWorkspaceTo(ws) after the commit, mirroring the
  pending_fork branch's switchWorkspace(newId).

- [P1] Warm-session live-editor slot hijack — /sessions keeps up to 3
  warm-mounted sessions; UserDraft stores one live editor per
  (workspace, kind). Each editor view unconditionally claimed the slot,
  so a hidden warm session in the same workspace+kind could overwrite
  the visible session's claim — chat actions like discard /
  "the open editor" then resolved to the wrong session. Fix: thread
  isActiveSession from SessionWrapper into Script/Flow/RawAppEditorView
  and gate setLiveEditorDraft on it.

- [P2] ForkDiffDrawer stale per-item raw diff cache — loadedDiffs /
  summaries persist for the drawer's lifetime; fetchComparison refetched
  on each open() but loadDiffFor short-circuited on cached keys, so an
  edit-then-reopen showed fresh counts but stale expanded content. Fix:
  clear both records at the top of fetchComparison.

Tests:
- sessionState.test.ts: 2 tests pinning commitSessionWorkspace's
  workspaceStore sync (mismatch and matching).
- userDraft.test.ts: 3 tests pinning the live-editor slot collision
  (regression), the active-session gate, and cleanup ordering.
- forkDiffCache.test.ts (new): 2 tests for the drawer cache
  invalidation contract via fetchComparison simulation.

Verified end-to-end in browser: P2 (close+reopen drawer triggered an
identical second batch of per-item get fetches), P1#1 (new-session send
from a fork synced localStorage.workspace to root and posted chat to
/api/w/local/...), P1#2 (raw_app slot for workspace=local correctly
follows the visible session across A→B→A switches while both stay
warm-mounted). svelte-check 0 errors; touched test suites green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:22:50 +02:00
Ruben Fiszel 32b4771f19 chore(main): release 1.713.1 (#9389)
* chore(main): release 1.713.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-01 09:10:53 +02:00
Ruben Fiszel b16828d480 chore(main): release 1.713.0 (#9369)
* chore(main): release 1.713.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-05-31 08:43:37 +00:00
Ruben Fiszel edf340c4d4 fix(security): re-pin cached hub scripts to CVE-patched versions (+ HUB_BASE_URL override for cache mode) (#9387)
* feat(cache): allow overriding hub base url via env in `cache` mode

The `windmill cache hubPaths.json` prebuild step (run in the Dockerfile) never
connects to the DB, so HUB_BASE_URL stays at its compiled default
(https://hub.windmill.dev) — unlike server/worker modes which load it from the
DB global setting. This made it impossible to point the prebuild cache step at
a private or staging hub.

Read HUB_BASE_URL from the environment at the start of cache_hub_scripts and
store it into the existing HUB_BASE_URL ArcSwap (the same static the hub fetch
functions read). No effect unless the env var is set and non-empty; server and
worker modes are unchanged (they still use the DB setting).

This also enables validating hub-script dependency changes end-to-end against a
local fake-hub before pushing to the real hub.

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

* fix(security): re-pin cached hub scripts to patched versions

windmill-integrations#133 was merged and pushed to the hub, minting new
versions with regenerated (CVE-free) lockfiles. Bump the hubPaths.json pins so
the prebuild cache step (`windmill cache`) fetches the patched lockfiles instead
of the old vulnerable ones (the hub serves each version_id immutably, so the old
pins keep returning the vulnerable deps until bumped).

- slackErrorHandler     19741 -> 28241
- slackRecoveryHandler   9080 -> 28239
- slackSuccessHandler   28220 -> 28240
- smtpReport             9086 -> 28242
- appReport             28076 -> 28243  (puppeteer screenshot script)
- gitInitRepo           28219 -> 28229  (already-fixed hub version; pin was stale)

Validated end-to-end against the real hub: `windmill cache` with these pins
produces a clean cache_nomount/bun (axios 1.16.1, form-data 4.0.5,
follow-redirects 1.16.0, nodemailer 8.0.10, ws 8.21.0, svelte 5.55.8,
devalue 5.8.1; basic-ftp and ip-address no longer pulled). No vulnerable
versions remain.

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-05-31 08:28:27 +00:00
Ruben Fiszel f0301b1605 feat(flows): preserve step/subflow worker tags under a custom-tagged flow (#9375)
* feat(flows): preserve step/subflow worker tags under a custom-tagged flow

A flow running on a custom worker tag force-propagates that tag to every
descendant step, script and nested sub-flow, overriding their own declared
tags. This made it impossible to route a specific step or sub-flow to a
different worker group. The new opt-in FlowValue.preserve_step_tags lets a
step that declares its own non-empty tag run on it; untagged steps still
inherit the flow tag. Defaults off to preserve existing behavior.

* chore: regenerate system prompts for preserve_step_tags

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(flows): nest preserve_step_tags toggle under flow worker tag setting

The toggle only affects routing when the flow has a custom worker tag, so
show it as a sub-setting of the Worker Group tag picker, visible only once a
tag is set, instead of as a standalone option.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(flows): allow step worker tag picker when preserve_step_tags is enabled

When a flow defines a worker tag, the per-step tag picker was replaced by a
read-only "Flow's WG" label. With preserve_step_tags enabled the step's own
tag is honored, so the picker must remain editable in that case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(flows): propagate preserve_step_tags to branch and loop bodies

payload_from_modules built the synthetic RawFlow for branch/loop bodies with
a default FlowValue, dropping preserve_step_tags. Tagged steps inside a
branch or loop therefore still inherited the parent flow tag even with the
flag enabled. Thread the flag through to the synthetic FlowValue so the
behavior is consistent for nested containers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(flows): clear preserve_step_tags when flow worker tag is removed

Avoids the flag lingering as invisible state after the flow tag (and its
toggle) are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(flows): repair preserve_step_tags propagation to branch/loop bodies

The previous commit added flow.preserve_step_tags at the payload_from_modules
call sites but the parameter and FlowValue field were not actually threaded
through (a failed edit left the function unchanged), so the crate did not
compile. This completes the change: payload_from_modules takes preserve_step_tags
and sets it on the synthetic FlowValue for branch/loop bodies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(flows): complete preserve_step_tags propagation to branch/loop bodies

Previous two commits left windmill-worker uncompilable: payload_from_modules
received flow.preserve_step_tags at its call sites but the parameter and the
synthetic FlowValue field were not actually added. This adds the parameter,
sets preserve_step_tags on the synthetic FlowValue, and threads
flow.preserve_step_tags through all five call sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(flows): clear preserve_step_tags whenever the flow worker tag is removed

The flag was only reset when the Worker Group toggle was switched off, not
when the tag was cleared directly in the picker (or via the YAML editor),
leaving preserve_step_tags=true as invisible state with the advanced badge
still reporting it active. Move the cleanup into the reactive block that
already tracks the flow tag so every clear path is covered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 12:10:16 +00:00
Ruben Fiszel def01b8ff6 fix(frontend): sanitize user markdown to prevent stored XSS (#9386)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:40:56 +00:00
Diego Imbert 2c0c2c467f fix(apps): make public apps opt into cross-origin isolation via wm_coep (GIT-884) (#9374)
* fix(apps): make public apps opt into cross-origin isolation via wm_coep

Public app pages served at /public/* and custom paths /a/* were not
getting the COEP/COOP/CORP headers, so they were blocked when embedded
as an iframe inside a cross-origin-isolated page (e.g. another raw app,
which sets Cross-Origin-Embedder-Policy: require-corp). A nested
document loaded into a require-corp context must itself set COEP for
the iframe to load.

Rather than applying the isolation headers to all public pages (which
would also force COEP on classic apps and break subresources without
CORP, e.g. external image URLs or embeds), public apps now opt in via
a `wm_coep` query param on the embed URL:

    <iframe src="https://<domain>/public/<ws>/<secret>?wm_coep=on">

The app publish drawer gains a URL/Embed toggle: "URL" shows the plain
shareable link (param-free), "Embed" shows a ready-to-copy iframe
snippet with wm_coep baked in, so the flag is discoverable exactly when
embedding and absent otherwise.

`wm_coep` is consumed internally and stripped from the app `query`
context so it doesn't collide with app-defined params. Only params we
own are stripped (an explicit set), not the whole `wm_` prefix.

Fixes GIT-884

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* nit

* nit

* fix(apps): only bake wm_coep into embed snippet for raw apps

AppEditorHeaderDeploy is shared by the classic (AppEditorHeader) and raw
(RawAppEditorHeader) deploy drawers. The embed snippet unconditionally
appended ?wm_coep=on, which for a classic/low-code app forces COEP
require-corp on the document and breaks no-CORP cross-origin subresources
(external <img> in AppImage/AppStatCard/AppNavbar, {@html} embeds in
AppHtml, CDN import() in AppCustomComponent) — the exact regression the
opt-in design avoids.

Add a `rawApp` prop (default false); the raw header passes rawApp. The
flag is appended only for raw apps; classic apps get a plain iframe
snippet, and the wm_coep helper text is shown only for raw apps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:05:19 +00:00
hugocasa 2bf11dcb15 feat(oauth): support per-provider sandbox URLs (#9358)
* feat(oauth): support per-provider sandbox URLs in registry + instance settings

* fix(oauth): polish sandbox review nits (cc lookup, header label, ee ref)

* refactor(oauth): drop dead build_oauth_clients duplicate in windmill-oauth

* refactor(oauth): derive sandbox-capable provider list from registry

* chore(docker): copy oauth_connect.json into frontend build stage

* test(oauth): cover sandbox helpers (as_sandbox, canonical_name, resolve)

* chore: update ee-repo-ref to 9297d8f790346e6a6ad540c7bca1a67f91ec11a2

This commit updates the EE repository reference after PR #595 was merged in windmill-ee-private.

Previous ee-repo-ref: 3ab3eca9ac15ebab6db991e7964bc5e48ce21f42

New ee-repo-ref: 9297d8f790346e6a6ad540c7bca1a67f91ec11a2

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-05-28 22:33:44 +00:00
Ruben Fiszel 889101b7f0 chore(main): release 1.712.0 (#9340)
* chore(main): release 1.712.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-05-28 19:52:36 +02:00
centdix 2553fbfe31 feat: add deepseek fim support (#9365) 2026-05-28 16:52:26 +00:00
Ruben Fiszel 9a659b636d fix(frontend): prevent duplicate asset node ids crashing flow graph (#9367) 2026-05-28 16:16:12 +00:00
Ruben Fiszel aea00611c4 fix(frontend): prevent MultiSelect crash on undefined value (#9364)
MultiSelect read `value.length` directly while `value` is a bindable
prop with no default, so a parent passing `undefined` (e.g. an
enum-array approval form field with no initial value via ArgInput)
threw a TypeError that blanked the entire approval page. Guard all
reads behind a `value ?? []` derived.

Fixes WIN-1996

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:48:54 +00:00