Commit Graph

14023 Commits

Author SHA1 Message Date
Ruben Fiszel e052d5ee45 fix(copilot): only warn on saved write_flow; add regression tests
Address review: writeFlowDraft reports conflicts/persistence errors as
{success:false} rather than throwing, so the empty-body warning must be
folded into the JSON result only on a successful save — otherwise the
model is told to set_flow_module_code on a flow that was never saved
(stale or nonexistent draft). Add core.test.ts coverage for the empty-body
warning (top-level, nested, preprocessor, failure; populated suppressed),
the no-warning-on-failed-save path, and the malformed-JSON escaping hint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:35:57 +00:00
Ruben Fiszel 71f17cea7f fix(copilot): stop write_flow forcing rawscript code into nested JSON
The global-chat write_flow tool made the model embed rawscript bodies
inside the modules JSON string, so code had to survive three levels of
escaping (tool arguments -> modules string -> content string). Models
routinely mangled the quotes/newlines and flow creation failed on the
first tries.

Bring write_flow to parity with flow mode's set_module_code escape hatch:
detect rawscript modules left empty or as inline_script placeholders and
tell the model to fill them via set_flow_module_code, add a code-escaping
hint to the JSON parse error, and update the guidance to keep code out of
the modules structure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:21:15 +00:00
Ruben Fiszel 68b1fcc5cd fix: prevent u16 underflow in suspend count causing permanent flow deadlock (#10256)
When extra resume_messages arrive concurrently and resume_messages.len()
exceeds required_events, the u16 subtraction wraps to ~65535, which is
written as the suspend counter and permanently deadlocks the flow waiting
for events that never arrive. Use saturating_sub so it clamps to 0 instead.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 12:14:10 +02:00
Ruben Fiszel 5685981c99 fix(tutorials): repair broken frontend tutorials after UI redesigns (#10255)
* fix(tutorials): repair broken frontend tutorials after UI redesigns

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

* fix(tutorials): wait for New menu anchor and drop vestigial async

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:49:08 +02:00
Ruben Fiszel 703744fb8b feat(ai-sessions): show item preview cards for tools (#10254)
* feat(ai-sessions): show item preview cards for create/update/open-preview tools

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

* fix(ai-sessions): only show open_preview card when the preview actually opened

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

* feat(ai-sessions): render preview card as a chip on the tool-call header row

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:47:53 +02:00
Ruben Fiszel d2c5d6f4b4 feat: make content search a full CE feature (#10252)
Content search (the `#` mode of the home-page Ctrl+K search, which
searches scripts/flows/apps/resources by content) was capped on CE to 10
scripts and 3 each of flows/apps/resources, with an "EE feature" warning
in the UI. It is now a full CE feature: the CE result caps are lifted to
match the previous EE limits (10000 scripts, 1000 each of the rest) and
the EE warning is removed.

Fixes WIN-2218

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:30:23 +02:00
Ruben Fiszel 380cf752ca fix(prompts): prefer Bun over Deno for TypeScript runtime selection (#10253)
Make the AI prompting instructions explicitly pick Bun as the default and
preferred TypeScript runtime, and treat Deno as the exception (only when a
script specifically requires the Deno runtime: Deno stdlib or deno.land URL
imports).

Previously the `write-script-bun` and `write-script-deno` skill descriptions
both read as equally valid TypeScript defaults ("MUST use when writing
Bun/TypeScript scripts" vs "MUST use when writing Deno/TypeScript scripts"),
giving no signal on which to choose for a generic TypeScript request.

Source-of-truth edits (system_prompts/utils.py LANGUAGE_METADATA,
languages/bun.md, languages/deno.md, cli/src/guidance/core.ts) then
regenerated via system_prompts/generate.py into the auto-generated skills,
prompts, and cli/src/guidance/skills.gen.ts.

Fixes WIN-2220

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:12:31 +02:00
hugocasa b948efd3c8 fix: accept ssh/scheme-less git repo urls and $var: refs in app repo resolution (#10246)
* fix(git-sync): accept ssh/scheme-less repo urls and $var: refs in app repo resolution

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(git-sync): interpolate repo urls at github-call sites only, not in persisted markers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: update ee-repo-ref to 183f78b3ee193d6b5e55fd453c570f94a12c8b13

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

Previous ee-repo-ref: 9bc5018f68edf3a9f256ef6315ad6ddf4fba3a45

New ee-repo-ref: 183f78b3ee193d6b5e55fd453c570f94a12c8b13

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-07-22 00:26:15 +02:00
Ruben Fiszel 50f1375f18 chore(main): release 1.765.0 (#10223)
* chore(main): release 1.765.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
v1.765.0
2026-07-21 20:15:14 +02:00
Diego Imbert ec6324409d fix(s3): support instance-policy credentials in object storage tests (#10238)
* fix(s3): cache ambient aws credentials and surface credential chain errors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCczeVBDhDnLXWqxPWw6od

* fix(frontend): clarify that object storage connection tests run on the server

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCczeVBDhDnLXWqxPWw6od

* fix: render test connection tooltip in popup and harden cache test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCczeVBDhDnLXWqxPWw6od

* better doc

* fix(frontend): correct tooltip wording, tests run from the executing worker

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCczeVBDhDnLXWqxPWw6od

* fix(s3): single-flight ambient credential provider creation

* fix(frontend): clarify object storage tests run on the server process

* fix(frontend): make instance object storage test tooltip provider-neutral

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-07-21 20:02:34 +02:00
hugocasa 2ce21c9ef8 feat(git-sync): enable per-item promotion mode on dev workspaces (#10205)
* feat(git-sync): enable per-item promotion mode on dev workspaces

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

* style: keep unrelated git-sync Alert copy at its original wrapping

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

* fix(git-sync): fall back to parent_path on empty deploy path + bump ee ref

computeGitSyncDeployBranch used ?? so a backend-serialized empty path (rename out of the repo filter) skipped the deploy branch and could commit to the tracked base; use || to fall back to parent_path like the backend. Bumps ee-repo-ref for the single-object promotion_open_prs fix (windmill-ee-private#679).

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

* fix(git-sync): route dev-promotion non-branchable objects off the tracked base

user/group objects (and any unresolvable ref) returned null in promotion mode, so a dev-workspace deploy pushed them straight to the parent's tracked branch. Fall back to the dev's env-label branch instead; the backend opens no PR for them (isolated, not promoted).

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

* feat(git-sync): dev-workspace promotion via a toggle on the inherited repo

A dev workspace reuses the single repo it inherited from prod: a 'Promote to prod via Git' toggle flips it between sync mode (deploys to the dev branch) and promotion mode (per-item wm_deploy/** PRs to prod), with a per-item/per-folder sub-toggle. Removes the redundant separate-promotion-repo setup for dev workspaces.

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

* test(git-sync): dev-promotion regression test + widen git_sync_e2e path filter

Adds a CLI integration case covering dev-workspace promotion (script -> wm_deploy branch; user/group -> env-label branch, main never touched). Widens the git-sync-test.yml relevance filter to the deploy-branch derivation, git-sync guard, and CLI git-deploy files so the e2e suite runs on PRs like this one.

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

* fix(git-sync): gate dev promotion toggle on EE, fix card mode + workflow path filters

Codex review: (1) show the dev promotion toggle only under an active EE license and revert the optimistic save if the backend rejects it; (2) derive the dev card's display mode from use_individual_branch so promotion copy shows in promotion mode; (3) mirror the new relevance paths into the workflow's top-level push/pull_request filters so it actually triggers.

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

* fix(git-sync): only use the single-card dev promotion UX when the dev has one repo

Codex review: an attached dev workspace keeps its own repositories rather than inheriting prod's. Gating the single-card + toggle + hidden-secondaries UX on repositories.length <= 1 makes a multi-repo attached dev fall back to the normal layout, so no active repo is hidden and an unrelated repo isn't presented as prod's promotion target.

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

* fix(git-sync): runtime EE-plan gate for promotion mode, consistent with auto-pull/PR

Codex review: promotion mode only had the CE compile rejection, while auto-pull and PR creation runtime-gate on the active plan (check_git_sync_ee_license). Add check_promotion_license and call it from both edit_git_sync_config and edit_git_sync_repository, plus the matching CE rejection on edit_git_sync_config so the two endpoints are symmetric. Promotion is now gated like every other git-sync EE setting.

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

* fix(git-sync): dev promotion must reuse the parent workspace's repository

Codex review: repository count doesn't prove a dev inherited prod's repo — an attached dev keeps its own. check_dev_promotion_targets_parent_repo resolves the promotion repo's URL and rejects enabling promotion unless it matches one the parent (prod) tracks, so branches/PRs can't target an unrelated repository. Called from both git-sync edit endpoints.

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

* fix(git-sync): dev promotion save-time check uses shared parent-repo matcher (url+branch)

Delegates to windmill_common::git_sync_ee::dev_promotion_target_matches_parent so the settings gate and the deploy-time safety net share one url+branch identity check. Bumps ee-repo-ref for the EE deploy-time enforcement.

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

* chore: bump ee-repo-ref for private resolve_repo_url_and_branch

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

* chore: bump ee-repo-ref for promotion-target matcher authz doc

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

* chore(git-sync): bump hub scripts to gitsync-cli versions, fix promotion tooltips

Point LATEST_GIT_SYNC_SCRIPT_PATH (28790 -> 28796) and
GIT_SYNC_PULL_SCRIPT_PATH / gitInitRepo (28789 -> 28795) at the hub
versions pinning windmill-cli@1.763.1-gitsync.0, which carries the
dev-workspace promotion routing. Slugs unchanged, so the GitHub-App
token check and hub script cache are unaffected.

Tooltips: enabling promotion pushes a PR-ready wm_deploy/** branch;
Windmill only opens the pull request itself when automatic pull
requests are enabled. Reword both toggles to stop promising a PR.

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

* fix(git-sync): dev promotion mirrors to the env-label branch, PR toggles exclusive by branch type

Bump ee-repo-ref for the dispatcher changes: a promotion dev's deploys
now also push to its env-label branch (one extra mirror job per batch,
users/groups mirror-only), and `fork_open_prs` no longer applies to a
dev in promotion mode where `promotion_open_prs` governs.

Frontend: the fork-PR toggle tooltip states its actual coverage
(wm-fork/** and the dev branch of a dev workspace) and that a promotion
dev's own pull request toggle takes over for wm_deploy/** branches.

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

* fix(git-sync): reject dev promotion on pre-28796 pinned sync scripts

An older pinned sync script bundles a CLI that force-disables per-item
branches on every fork, so enabling promotion on a dev workspace with
such a pin would silently keep deploying to the env-label branch. Both
git-sync edit endpoints now reject the combination with an actionable
error; the EE dispatchers (via ee-repo-ref bump) demote inherited
configs to promotion-off semantics so markers, branch keys and the
mirror match the branch the CLI actually pushes. Roots and auto-managed
repositories are unaffected.

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

* fix(git-sync): serialize dev promotion toggle saves

The promotion and per-folder toggles persist immediately via whole-repo
saves; leaving them interactive while one is pending lets rapid flips
race, and the earlier save (enabling runs extra backend checks) can
commit last, silently reversing the state the UI shows. Both toggles now
disable while a save is in flight.

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

* fix(git-sync): lock auto-PR toggle during promotion save, rename-out branch routing

Frontend: the automatic-PR toggle is revealed by the promotion toggle's
in-flight save; an edit made mid-save was absorbed into the saved
baseline without reaching the backend. It now disables during that save.

EE (ee-repo-ref bump): dispatcher debounce/concurrency keys and PR
markers follow the CLI's parent_path fallback for rename-out items, so
their wm_deploy/** branches debounce per-branch and open their PR.

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

* chore(git-sync): condense comments to durable constraints

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

* chore: update ee-repo-ref to 8bf73f803158bcbf7b8d55a36f4a1ebfcc1bbcd9

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

Previous ee-repo-ref: c2cd718cb53d234f909f485bd7cd43ed9605ffd1

New ee-repo-ref: 8bf73f803158bcbf7b8d55a36f4a1ebfcc1bbcd9

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-07-21 17:07:52 +00:00
Guilhem 9739d5a2c2 feat: unified read-only diff chat tool (drafts, fork vs parent, search) (#10211)
* feat(frontend): unified `diff` chat tool with cached snapshot, fork mode, and search

One read-only global-chat tool for every comparison: drafts vs deployed
(workspace index + per-item unified patches over stable YAML), deployed
fork vs parent workspace (against="parent_workspace", sharing the fork
banner's compareWorkspaces fetch through a single-flight store), and a
literal grep over changed diff lines. Multi-file raw apps split into
per-file text patches with folder-style index children and per-file
reads. Patches are materialized once into a per-workspace cache keyed on
draft created_at / comparison ahead-behind markers and the workspace
drafts version, so repeated queries never refetch unchanged content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai_evals): teach the mock draft backend what the diff tool reads

The diff tool reads drafts through the get_draft overlay, the drafts
listing's draft_only flag, and per-row created_at change markers — none
of which the benchmark mock modelled (fixed timestamp, always
draft_only, overlay ignored), so in evals every draft looked absent and
the model looped to max turns. Mirror production: monotonic
deterministic created_at bumped per upsert, draft_only computed against
the deployed stores, and draft/no_deployed overlays on script/flow/app
reads (404-shaped not-found). Also drop the diff case's judge items
about conversation content the judge never sees — tool usage is already
enforced deterministically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): diff tool reads unsaved editor state instead of going stale

The pre-diff flush honors the auto-save toggle (a read-only tool must
not persist parked edits), which left a gap: with auto-save off — or
after a failed save — the persisted draft the diff reads is stale, and
a brand-new editor-only draft looks absent. Item reads now detect
unflushed parked edits (hasUnsavedDisabledChanges / failed save state)
and diff the in-memory editor value directly, bypassing the snapshot
cache (it must only hold persisted state) with an explicit unsaved-
changes note; index and search modes warn which items' unsaved edits
they exclude. Local values are canonicalized onto the persisted draft
shape so they never diff noisily against the deployed side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ai): invalidate diff cache the moment any draft write lands

The snapshot cache leaned on time windows (5s listing throttle, 15s
read reuse) to notice writes it didn't trigger itself — an editor
autosave landing between two diff reads could serve the pre-edit patch.
The syncer now exposes onAnySaved (fires for landed upserts AND
deletes, all keys), and the snapshot subscribes once: a landed write
marks exactly that item's patch stale and expires the listing throttle,
so the next read refetches regardless of any reuse window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): reject the diff file arg on single-document items

Passing file for a script/flow/classic-app diff was silently ignored
and returned the whole patch — an explicit error steers the model to
call again without it. Also declares the file arg on the item handlers'
signatures it was already flowing through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): surface empty-file additions/deletions in app diffs

An empty file appearing or disappearing produces no text patch, so the
per-file split dropped it — a draft whose only change was such a file
read "unchanged". Presence changes now keep their added/deleted entry
(patch '', 0 lines), render as "(empty file)" in summaries, and a file
read states the presence change instead of an empty window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): include classic-app drafts in the diff index and fix ++/-- search

itemTypeForKind now maps classic `app` draft rows to the chat app type
(mirroring the read path, which already pairs app/raw_app), so their
diffs materialize in the index and search instead of reporting "not
addressable". Changed-line search is hunk-aware: `---`/`+++` file
labels only occur before the first @@ marker, so a changed source line
like `++counter` (rendered `+++counter`) now matches instead of being
mistaken for a label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): mask every variable value in chat diffs; compare classic apps value-to-value

Variable VALUES never reach a tool result — the chat-wide invariant
read_workspace_item enforces, not just for secrets. Draft-mode diffs
mask both sides with a placeholder pair that still marks WHETHER the
value changed; fork-mode masks at fetch (and still never decrypts);
item reads carry an explicit note. The former secret-only flag is now
valueMasked.

Classic-app drafts hold the bare grid value while the deployed row
nests it beside summary/policy — diffed raw, a one-field edit read as
a whole-document rewrite. Both sides now reduce to { value } via
classicAppDraftValue (pure, unwraps legacy wrapped drafts), which also
cleans the CompareDrafts drawer for classic apps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): honest secret-draft reporting, classic-app metadata split, glob-safe file subjects

A secret variable's sides are both masked upstream, so an empty patch
cannot prove the value is unchanged — such drafts now report
"cannot be compared; may differ" (valueUncomparable) instead of
"matches deployed", in the index and item reads.

Classic-app drafts mirror summary/draft_path into the bare grid while
the deployed row keeps summary as a column: sides now reduce to
{summary, value} via classicAppDraftParts, applied to both sides, so a
summary edit diffs as one and draft-only markers never pollute the
grid diff.

Raw-app search subjects strip the file key's leading slash so
slash-anchored globs like f/x/*.tsx match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): classic-app local edits, comparison-relevant fork fields, conflicts as unflushed

The chat app type spans two draft kinds: item mode now flushes and
probes both raw_app and classic app keys, and the flush sweep includes
classic-app editor cells (kept out of GLOBAL_DRAFT_KINDS so
clearGlobalDrafts never clears an open classic editor).

Fork projections gain the fields the backend comparison counts that
getItemValue drops: flow schema (with a taxonomy-agnostic inline-hash
strip) and resource-type description/format_extension/is_fileset.
Folder display_name is not exposed by the API's Folder type, so it
cannot be projected.

A conflicted save leaves its payload parked with state 'none', so
index/search now count conflicts among unflushed paths and say so in
their warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): staged app renames diff as path; flush classic-app cells at their real keys

A staged rename (draft_path) changes where deploy lands an app, so both
app kinds now compare `path` on both sides — a rename-only draft diffs
instead of reading "matches deployed". classicAppDraftParts returns the
staged path separately from the grid.

Item mode resolves each draft kind's own storage path and additionally
asks the listing which row owns a friendly/renamed path — a renamed
classic app's cell lives at its ORIGINAL storage path, which only the
listing knows — so pending/failed/auto-save-off edits are flushed and
probed at the real keys. The appDiffSides rationale comment is
compressed to the repo's four-line limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): never claim folder parity the API cannot prove

folder.display_name exists only as a DB column — no folder endpoint
returns it — so an identical projection cannot prove a fork folder
matches its parent. Fork index and item reads for folders now say the
display name is not exposed and may be what differs, instead of
"content matches parent". Exposing the field on getFolder is a backend
follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): gap-free patch pagination; forced-fresh comparisons never join older fetches

When the char backstop cut inside a patch window, the continuation
offset still pointed past the requested window — silently skipping the
undelivered lines forever. windowPatch now cuts at the last complete
line and continues exactly there (a single over-budget line is
delivered truncated and stepped past so pagination always advances).

fetchWorkspaceComparison treats an in-flight request as being as old
as its start: maxAgeMs now gates joining it, so a freshness-forced
post-mutation read (maxAgeMs 0) always issues its own fetch instead of
adopting a tally that began before the mutation, and a superseded
request can no longer clobber a newer cached result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): generation-ordered comparison writes; path-only fork reads for enum-less kinds

Concurrent comparison requests can share a Date.now() value, letting a
superseded request's late result overwrite a newer one and be reused
for 30s — cache writes are now ordered by a monotonic request
generation. Test pins the same-millisecond race with the newer request
resolving first.

Fork comparison kinds outside the chat type enum (folder,
resource_type, …) were listed and even advertised as readable but no
call could reach them: a fork item read without `type` is now a
path-only wildcard (ambiguous paths list their kinds and ask for
type), messages label entries by their comparison kind, and pending
index lines for enum-less kinds advertise the path-only read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): grid-based wrapper detection, comparison invalidation on mutations, multi-kind wildcard reads, honest hidden-diff summaries

The classic-app wrapper heuristic keyed on metadata keys the editor
mirrors into every bare grid — a grid with a component named `value`
was reduced to that component. `grid` presence is the discriminator: a
bare App always has it, a legacy wrapper never does.

invalidateWorkspaceDrafts now also drops cached fork comparisons for
the workspace, so the FIRST post-deploy fork read cannot reuse a
banner-prewarmed pre-deploy tally (the snapshot-baseline check only
covered subsequent reads).

Wildcard fork reads return a section per matching kind instead of an
unactionable "pass type" for kinds the chat type enum cannot name, and
the fork index never summarizes ACL-hidden differences as parity —
hidden counts stay directional (a conflicted item counts in both).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): address cubic review batch — invalidation scope, races, edge output

Comparison cache: invalidation matches either side of the pair (a
parent deploy moves its forks' tallies), fences in-flight requests
(no new joins, late results rejected via a per-key generation floor),
and the map is LRU-capped. Eviction moves from every drafts-version
bump to deploy success only — draft saves never move the deployed
tally. Fork snapshots also baseline the PARENT's drafts version.

Draft materialization carries a stale-generation token so a save
landing mid-fetch discards that run's pre-save result instead of
repopulating the invalidated entry; a save/delete also expires the
fork cache's hasLocalDraft join. onAnySaved listeners are
error-isolated (a throwing listener must not mark a committed save
failed) and the pagehide keepalive flush notifies them on dispatch.

Output edges: folder fork lines drop the empty parenthetical, and a
patch-window offset past the end reports itself instead of an
impossible range. The eval case pins the diff call's path argument.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(ai): one fencing primitive per cache instead of per-surface races

Review rounds kept finding pairwise races between async producers and
invalidation — each patched with its own fence. Replace the class:

- diffSnapshot: a per-workspace mutation epoch, bumped by every
  invalidation. Both reconcilers run a bounded retry loop — joiners
  re-validate after awaiting, producers refuse to store results whose
  inputs predate a mutation. Covers in-flight listing adoption and
  pre-deploy fork tallies in one mechanism.
- workspaceComparison: per-WORKSPACE generation floors (either side of
  a pair). Any request started before an invalidation is fenced from
  joining and from landing in the cache — including superseded
  requests the inflight map no longer tracks.

Also: delete_workspace_item invalidates comparisons like deploy does
(deployed state moved), and empty FILTERED indexes say the filter
matched nothing instead of claiming workspace/fork parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): invalidate comparisons on every direct deploy; keep secret caveat with metadata changes

Direct chat deploys (schedule/trigger/resource/variable/app) bypass
deployDraftToWorkspace and never evicted cached fork comparisons — the
shared deploy tail now invalidates before the fallible draft cleanup.

A secret variable whose metadata also changed produced a non-empty patch
that silently dropped the value-uncomparable caveat; item reads, the
index, and fork sections now keep the caveat alongside the patch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): scope diff caches to the authenticated identity; derive fork freshness from the comparison store

An SPA logout/login left workspace-keyed diff caches (per-user drafts,
permission-filtered fork patches) readable by the next account — both
cache modules now wipe on identity change, with a global generation
floor fencing requests started under the previous account.

The fork snapshot stamped its own fetchedAt over a comparison that
could already be near expiry, compounding the two 30s windows, and
survived comparison-store invalidation when draft cleanup failed after
a deploy. It now carries the comparison's own fetchedAt/generation and
stops reuse the moment the store fences it. Delete-item invalidation
moved before the fallible draft cleanup for the same reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): fence fork-reconciliation joins across account switches

ForkCache lacked the epoch stamp WorkspaceCache carries, so a joiner
arriving after an identity change (or any epoch bump landing before it)
compared its own post-bump epoch against itself and adopted the old
producer's in-flight tally. The cache now records its producer's epoch
for the joiner and reuse gates, and an identity change also discards
the in-flight reconciliation maps so no cross-identity join exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): surface swallowed fork-side fetch failures; include conflicted editor edits in item diffs

The shared getItemValue reads {} for any failed fetch, so a transient
API failure on a fork side rendered as a fabricated one-sided diff (or
parity when both sides failed). A fork side is only fetched when the
comparison lists it as existing, so an empty read now raises and shows
as a fetch-error entry.

Item reads promised conflicted local edits (the index says so) but the
local-override branch only covered autosave-off and failed saves — a
conflict silently fell back to the persisted draft. Conflicts now read
the in-memory editor value too, with a caveat naming which side is
shown either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): report failed diff materializations as unsearched instead of silently omitting them

A side-fetch failure left an index entry with status 'error' and no
patch; diff search skipped it and still presented definitive no-match
or complete-count results. Failed entries are now listed in a warning
naming what was not searched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 17:06:32 +00:00
Guilhem d6cf1ef987 feat: attach text files to chat messages, read on demand via file tools (#10215)
* feat: attach text files to chat messages, read on demand via file tools

* fix: resolve name collisions and sync message files with the transcript

* refactor: render message file chips with the shared context badge

* fix: suffix same-name attachments, sync registry on compaction, bound file bytes

* feat: carry message files across compaction, drop them from the roster

* refactor: merge context, dom and file badges into one wrapping row

* fix: dedupe identical attachments and make badge list keys collision-proof

* fix: name carried files inside the collapsed summary instead of badges

* fix: serialize registry reconciliation and correct the attachment budget

* fix: reserve pending bytes so overlapping reads honor the attachment budget

* fix: share attachment byte budget across concurrent composers

The bottom composer and the edit box are both mounted while editing an
earlier message, but each enforced MAX_CONVERSATION_FILE_BYTES against
only its own staged files plus the transcript. Two attaches near the cap
could each pass independently and overflow the persisted transcript.

Each composer now publishes its staged bytes (committed attachments +
in-flight reads) to the manager, keyed per instance, and the attach-time
budget subtracts every other live composer's stage. A message an open
composer is editing is skipped from the transcript sum since that
composer's stage stands in for it.

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

* fix: charge an edited message at its persisted size until the edit commits

An edit is not committed until send, so the edited message's persisted
attachments return if the edit is cancelled. Substituting only the edit
box's (possibly emptied) stage let the always-mounted bottom composer
claim headroom that vanishes on cancel: remove the near-limit files in
the editor, fill the bottom draft, cancel, and the persisted transcript
overflows MAX_CONVERSATION_FILE_BYTES.

attachmentBytesExcluding now charges a message another composer is
editing at max(persisted size, editor stage), so freed space only
becomes available once the edit actually commits.

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

* fix: preserve a message file's exact name when a session row clashes on rebuild

syncMessageScoped rebuilds message-scoped rows from the transcript through
collision-suffixing addFiles. Session rows load first (on restore), so one
holding a wanted name pushed the rebuilt message row to a "(2)" suffix while
the persisted prompt still referenced the bare name — get() then resolved the
reference to the session asset and the model read the wrong content.

Free the name from the conflicting session row before the rebuild so the
message row reclaims its exact reference. The rename is in-memory only: it is
deterministic and re-applied on every load, and the session roster is
regenerated live each send, so the session asset stays addressable under the
suffix without a persisted-record update.

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

* fix: reserve resent files across the edit-resend gap

The edit box unmounts (dropping its staged-byte entry) the instant the user
submits an edit, but restartGeneration then awaits registry sync and beforeSend
before the optimistic bubble lands in the transcript. During that gap the
resent files were reserved nowhere, so the always-mounted bottom composer could
attach into the temporary headroom and the resend would then push the persisted
transcript past MAX_CONVERSATION_FILE_BYTES.

restartGeneration now reserves the resent files' bytes in shared manager state
before the transcript slice; sendRequest releases the reservation once it
installs the bubble (or restores the files to the composer on a pre-install
bail). The reservation bridges the gap so the budget stays honored throughout.

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

* fix: re-target an in-flight index when a session row is renamed on rebuild

#freeNameForMessageRow renames a restored session row so a same-named message
attachment can reclaim its exact name. But the row's #indexFile, started under
the old name during restore, stamps via #patchFile(oldName, file) — after the
rename that no longer matches, leaving the row stuck 'indexing' so read_file
rejects it and search_files excludes it.

Re-kick #indexFile under the new name when the renamed row is still indexing;
the stale completion then no-ops (its name is gone).

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

* fix: release resend reservation on local-command sends; surface compaction-orphaned files

Two follow-ups to the message-attachment work:

- A resend edited to /clear or /compact runs the local-command path and returns
  before installing a bubble, so the #RESEND_KEY reservation set by
  restartGeneration was never released and its bytes stayed charged, blocking
  later attachments. Release it on every sendRequest path that exits before
  install (via #releaseResendReservation).

- Drop-oldest compaction (summary fallback) removes API messages without a
  summary, so a folded message's `## ATTACHED FILES` reference no longer reaches
  the model even though the file stays readable. The roster omits message-scoped
  files, so the model loses awareness of them. orphanedMessageFileNames() finds
  message files whose only referencing message went negative-index, and the
  roster now advertises them (summary compaction already carries its own).

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

* fix: key the resend reservation per send so unrelated sends can't release it

The resend reservation used a single shared key, so a normal or concurrent
sendRequest released it at its own install/early-return even though it didn't
own it — dropping an in-flight resend's reservation and letting attachments
staged before the resend bubble lands under-count against the byte cap.

restartGeneration now mints a per-resend token, reserves under it, and threads
it through sendRequest as resendReservationKey; releases act only on that key.
A send with no token (every normal send) releases nothing.

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

* fix: reserve the whole wanted-name set when freeing session rows on rebuild

Freeing a session row for a message-scoped rebuild suffixed it against current
rows only. With the transcript referencing both notes (2).md and notes.md and a
session row named notes.md, freeing notes.md renamed it onto notes (2).md — also
a wanted reference — so that message row cascaded to notes (3).md while its
persisted reference stayed notes (2).md, and read_file returned the session file.

#uniqueName now accepts a reserved set; the rebuild frees each session clash
clear of the entire wanted-name set, so every message row reclaims its exact
reference regardless of collision order.

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

* fix: orphan summary-carried files when drop-oldest removes the summary

orphanedMessageFileNames classified summary rows as always-live, but a summary
carries its folded files' reference on its own API message. When summary
compaction succeeds and a later summarization fails, drop-oldest can remove that
API message, yet the files stayed off the roster — so the model lost their
reference even though they remained readable.

The summary display row now tracks its API index (slot 0 at creation, re-based by
drop-oldest); a negative index reads as "counterpart gone" and its files move to
the roster, mirroring user-message orphans. The index is used only for orphan
detection, never as a restart target.

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

* fix: reserve outgoing file bytes for normal and queued sends too

The resend reservation covered edit/retry, but a normal or queued send has the
same gap: the composer (or queue) clears its files the instant sendRequest is
called, dropping the staged-byte entry, while sendRequest then awaits
regrantLocked()/refreshFolders() before the bubble lands. With a locked or slow
linked folder the composer stays enabled, so a fresh drop can spend the same
headroom and overflow the 5 MB cap once the first bubble installs.

Generalize the reservation: sendRequest mints a per-send token and reserves the
outgoing files' bytes just before attachment upkeep (reusing restartGeneration's
token when present), and releases it on install or any pre-install exit.

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

* refactor: join attachment layers on a stable content-hash id

Sixteen review rounds kept finding the same bug family: a message file's
identity was its display name, joined by hand across the registry, the
transcript, the prompt reference, and the render keys — every same-name
collision or interleaved rebuild made two of those copies disagree.

Give each message attachment a deterministic id, attachedTextFileId(name,
content) — a synchronous pure-JS hash (works on plain-HTTP deployments) whose
exact value is pinned by test, since persisted transcripts reference it. The
prompt and roster list the id, the file tools resolve id-first (bare names
remain a fallback for legacy chats and session links), and pre-id transcripts
hydrate on load by recomputing the same hash — no migration state.

Names become display-only and may collide freely, which deletes the machinery
that defended them: the suffix-readback registration loop, session-row renaming
on rebuild (#freeNameForMessageRow, reserved-set #uniqueName), and the
reconciler's serialization guards (#syncSeq/#syncChain) — syncMessageScoped now
compares ids instead of awaiting blob text, so it is synchronous and cannot
interleave. A same-name clash within one draft gets a composer-local courtesy
rename before the id is minted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: resolve bare names to session rows and scope id searches to one row

A bare name is the roster's namespace: session links are advertised by
filename and have no other handle, so a same-named message attachment (which
is addressed by id) must not shadow them. get() now resolves session rows
first, keeping the message-row name lookup only as the fallback for
transcripts persisted before ids existed.

search_files restricted an id reference by mapping it back to the display
name and letting the worker filter on it — same-named files were then
searched together under one label. The tool now passes the resolved row
itself, so an id-scoped search can only ever hit its own file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: label search hits with resolvable ids, normalize attach batches at commit

An unscoped search_files reported hits by display name only. Names may
collide, so a hit could not be mapped back to the row that produced it —
a follow-up read_file on the bare name could return a different same-named
file. Rows carrying an id are now labeled `name (file id: …)` in hit lines,
so every hit names the reference that resolves to exactly that row.

addTextFiles normalized (deduped, courtesy-renamed) each file against a
snapshot taken during its read loop. Attach batches overlap, so a file
committed by another batch between reads escaped both checks — duplicate or
same-named unsuffixed entries in one message. Normalization now runs in the
single synchronous commit step against the live list (foldIntoDraft), where
nothing can interleave.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: dedupe renamed re-drops in foldIntoDraft, truncate queued file chips

"Same file dropped twice" means same original (name, content), but a
courtesy rename erases the original name — an identical re-drop then missed
the duplicate check and landed as a further-suffixed copy. The dedupe now
also matches entries whose suffix-stripped base name equals the read's name.

Queued file chip labels get min-w-0 so long filenames truncate inside
max-w-36 instead of overflowing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address cubic review — line counts, chip clicks, reference robustness

Five fixes from the cubic pass:

- The prompt advertised split('\n').length lines, one more than read_file
  reports for newline-terminated files — textLineCount now matches the tool's
  numbering (0 for empty, no phantom trailing line).
- Clicking a sent message's badge opened edit mode (the wrapper's
  click-to-edit), unmounting the preview popover as it opened; the badge row
  now keeps clicks to itself.
- resolve() accepts the composite label rosters and search hits print
  (`name (file id: x)`) — models echo references verbatim, so the printed
  form must resolve.
- fileToAttachedTextFile enforces MAX_TEXT_FILE_BYTES itself (raw size +
  decoded byte length), so no ingestion path can persist an oversized
  attachment past the composer's pre-check.
- Duplicate detection after a courtesy rename now uses an explicit sourceName
  instead of inferring provenance from the display name — a user's real
  `report (2).md` is not a rename of `report.md`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: exact names win over label parsing, commit recheck uses decoded bytes

resolve() parsed any `name (file id: x)`-shaped reference as a printed label,
so a session file literally named that way became unreachable by its exact
name (the dead-id fallback resolved the base name instead). Exact id and
exact-name lookups now run before label interpretation.

Attachment admission and the pending reservation use raw File.size, but the
committed charge is the decoded UTF-8 length — malformed input decodes each
invalid byte to a 3-byte replacement character, so a file passing the 8KB
text sniff could inflate past the conversation cap. The synchronous commit
step now re-checks the live budget against decoded sizes
(admitWithinByteBudget) and drops what no longer fits, with the budget toast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: normalize files folded into the queued message

Repeated submissions during a stream aggregate into one queued message, but
their files were concatenated raw: an identical re-attach duplicated its chip
and ate a slot (possibly displacing a distinct file at the eight-file cap),
and a same-name clash skipped the courtesy rename. The queue now folds new
files through the same commit normalization as the composer — the queued
entry is a message draft like any other.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: compaction boundary label uses text-normal text-2xs

* fix: fold provenance survives pass-through, dequeued files fold into the draft

foldIntoDraft recorded sourceName only for renames it performed itself, so a
file already courtesy-renamed by the composer lost its provenance when folded
into the queue — a later re-attach of the original escaped dedupe. Folds now
compose: the original source name rides through every fold, and dedupe
matches on it.

dequeueMessage restored queued files into a possibly-populated composer by
raw concatenation; prependText now folds them like every other draft
aggregation (dedupe, courtesy rename) before applying the cap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: one MessageDraft owns the lanes that ship with a send

Review rounds kept finding the same P2 shape: an aggregation point where
files join a draft (composer commit, queue append, dequeue restore) that
forgot one of the draft rules — fold dedupe, courtesy rename, slot caps,
byte admission, lanes moving together. The rules existed only as convention
re-implemented per site.

MessageDraft owns them once: text, pastes, images, and text files live on
one object with addFiles (fold + optional decoded-byte admission + cap),
addImages (cap), prepend (restore-merge), replaceIfEmpty (occupied-guard
restore), and take (all lanes leave together). The composer holds a draft
instead of four state vars, and the queue is a draft behind the existing
queuedMessage/queuedImages/queuedFiles accessors — an aggregation point can
no longer skip a rule, because there is no raw array to concatenate into.

Deliberately not moved: @context and DOM picks (ContextManager owns their
lifecycle), the conversation byte budget's cross-composer ledger (store-side
follow-up), and sendRequest's options shape (it decomposes immediately and
is pinned by the manager test suite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: correct the drop-routing comment, condense the budget doc

* fix: address cubic review — name sanitization, drop hold, merge restores

Four fixes from the cubic pass:

- Attachment display names render into model-facing prompt blocks, and OS
  filenames may legally contain control characters — sanitizeAttachmentName
  strips them at attach and again at every prompt-render site (legacy names
  predate the attach-time pass), so a crafted name cannot inject prompt lines.
- Drop routing awaits handle/entry resolution before it can call
  addTextFiles; a send during that window landed the dropped files on the
  next message. The drop handler now holds sending (holdSendForIngestion,
  taken before the first await) until routing completes.
- Restoring a taken queue after a failed auto-send replaced the queued draft
  wholesale, silently losing a follow-up queued during the preflight. Both
  #restoreQueue and the unmounted-input requeue now merge via draft.prepend —
  the taken entry's text lands above the newer follow-up.
- restartGeneration validated the API restart index only after reserving the
  resend bytes and truncating the transcript, so a stale index threw with the
  reservation leaked and the display transcript half-mutated. The index is
  resolved and validated before anything is touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: restored drafts keep chronological priority, store names stay resolvable

A failed auto-send's restore folded the taken (older) draft's attachments
AFTER a follow-up queued during preflight, so at the slot caps the older
attachments silently dropped despite the text landing first — and only one
entry's pinned context survived. prepend() now puts the restored lanes ahead
(the cap drops the newest additions) and #restoreQueue unions both pinned
contexts by identity.

Session filenames were sanitized only at prompt render, so an id-less file
whose stored name carries control characters was advertised under a name
that resolve() could not match. Names are now sanitized at every store
row-creation site (attach, folder expansion, refresh, and persisted-row
restore for pre-sanitization records), making the advertised name the stored
name everywhere — render-site sanitization remains as defense in depth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: update ee-repo-ref to aaa6cb89b05b76139252c64f057e53b94d12ac60

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

Previous ee-repo-ref: 4c08634af953db5c1125b1fb03f5af211fe21db3

New ee-repo-ref: aaa6cb89b05b76139252c64f057e53b94d12ac60

Automated by sync-ee-ref workflow.

* chore: repin ee-repo-ref to main's eb3690a34b (aaa6cb89 needs the unmerged AmqpTrigger OSS companion)

* Revert "chore: repin ee-repo-ref to main's eb3690a34b (aaa6cb89 needs the unmerged AmqpTrigger OSS companion)"

This reverts commit a782e6f1c5.

* refactor: one name seam per scope — claim on the store, sanitize in the fold

Name rules (sanitize, uniquify, dedupe, resolve) lived as convention at every
creation site: four sites carried verbatim copies, two skipped uniquify
(restore, file placeholders — legacy names could collapse to one display
name), and the dedupe check compared the raw name against sanitized stored
names, so re-linking a control-char file added a spurious copy.

Store side: #claimName(raw) = sanitize + uniquify is now the only way a
session row gets its display name; addFiles derives the sanitized name once
at the top of the loop, so dedupe, uniqueness, and the stored row all see the
same string, while relPath and folder keep the raw on-disk keys they must
match. Draft side: foldIntoDraft sanitizes its reads itself instead of
assuming the reader did, making the fold self-contained. Folder names — raw
grouping keys by design — are sanitized at their model-facing render sites
(roster folder lines, not-found listing), mirroring rosterLine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: a display-name collision is not a duplicate

Sanitizing names before the dedupe compare made two DISTINCT files whose raw
names sanitize identically look like re-links — the second was silently
discarded instead of claiming a suffixed name like the restore path does.

Dedupe now means "the same file re-linked": matching stats (size + mtime)
plus a matching name, pre-suffix sourceName, or raw path. #claimName records
the pre-suffix name when uniquifying renamed a row, so re-linking a suffixed
file's original still dedupes. A same-named file with different stats links
as its own row rather than being silently swallowed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: the re-link identity is the raw name, persisted with the row

Dedupe still compared sanitized display names, so two distinct raw names
that sanitize identically collapsed whenever their stats also matched — and
the provenance recorded for suffixed rows lived only in memory, so after a
reload re-linking the original behind a suffixed row stacked another copy.

sourceName now records the RAW pre-sanitization name on every session row
(display names lose information twice — sanitize, then suffix), rides the
persisted record, and is re-derived on restore. Dedupe matches stats plus
raw identity (sourceName, or relPath for folder children) and never compares
display names. #claimName returns to a pure name function; restore claims
the display name from the persisted raw identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-07-21 17:06:21 +00:00
hugocasa dae7c49f21 test(git-sync): cover fork-of-dev-workspace branch naming and routing (#10231)
* test(git-sync): cover fork-of-dev-workspace branch naming and routing

A throwaway fork of a dev workspace pushes to `wm-fork/<tracked>/<id>` (the
tracked branch, not the dev's label), and the root's `sync_forks` poller
enumerates `wm-fork/<tracked>/*` and routes commits on that branch into the
nested fork through the root. This was twice assumed to instead live on
`wm-fork/<dev-label>/<id>` and therefore never be collected/reconciled; these
tests pin the real behavior.

- CLI unit: `computeGitSyncDeployBranch` for a fork whose parent is a dev
  workspace resolves to `wm-fork/main/<id>`, explicitly not `wm-fork/dev/<id>`.
- git-sync E2E: fork a dev workspace, assert the created branch is
  `wm-fork/main/<id>` (not `wm-fork/dev/*`), then assert a commit on it deploys
  into the fork via the root's sync_forks poller while the root is untouched.

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

* test(git-sync): reconcile single-dev-per-root in fork-of-dev e2e

The root workspace allows only one dev workspace, and a sibling test leaves one
attached, so attach_dev_workspace failed with "already has a dev workspace".
Detach any pre-existing dev before attaching, and detach ours via addCleanup so
the test doesn't leak its own.

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

* chore: update ee-repo-ref to aaa6cb89b05b76139252c64f057e53b94d12ac60

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

Previous ee-repo-ref: 4c08634af953db5c1125b1fb03f5af211fe21db3

New ee-repo-ref: aaa6cb89b05b76139252c64f057e53b94d12ac60

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-07-21 17:04:39 +00:00
Guilhem 7ac27c1ef2 fix(frontend): limit compare & deploy rows to the active direction (#10234)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 17:04:15 +00:00
Ruben Fiszel 4a898247a2 fix(apps): let entitled viewers read pre-existing S3 files from deployed apps (#10245)
* fix(apps): let entitled viewers read pre-existing S3 files from deployed apps

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

* fix(apps): confine S3 viewer fallback to full unscoped sessions

A scope-restricted token (e.g. apps:read:<app>, or an app-embed token) is
allowed on apps_u/* but rejected by the route-scope middleware on job_helpers/*,
so granting it the viewer fallback would be a new capability it cannot obtain
directly. Gate the fallback on scopes.is_none() so only full sessions (which can
already read via job_helpers) delegate; scoped and anonymous callers stay gated.
Add a scoped-token isolation assertion.

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

* fix(apps): treat filter-tags-only tokens as unscoped for S3 viewer fallback

The scopes.is_none() guard wrongly denied the viewer fallback to tokens that are
effectively unscoped (empty scope arrays and if_jobs:filter_tags:-only tokens),
which the route-scope middleware treats as unrestricted and which can therefore
read the same file via job_helpers directly. Reuse that semantics via a shared
is_effectively_unscoped helper so the relaxation covers exactly the tokens that
gain no new capability, while genuinely scoped tokens stay gated.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:04:01 +00:00
Diego Imbert 555c751016 fix(db): repair s3 asset paths missing default-storage leading slash (#10243)
* fix(db): repair s3 asset paths missing default-storage leading slash

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01An2pTqSmqJd2XwnagvX4kM

* fix(db): also repair script_trigger refs + exclude _default_ storage alias

Extend the s3 leading-slash repair beyond the asset table:

- script_trigger.trigger_ref (pipeline cascade edges, stored as s3://<path>)
  suffered the identical corruption: a window-era default-storage edge was
  recorded as s3://exports/x instead of s3:///exports/x, so it no longer
  matches the producer's post-fix write ref at dispatch (asset_dispatch does
  an exact trigger_ref = match with no DISTINCT), silently breaking the edge.
  Repaired with the same storage-name heuristic, with a dedup DELETE to avoid
  double-dispatch.

- Exclude the reserved _default_ alias from the storage-name set. The runtime
  treats s3://_default_/key as the primary storage (fork_storage_ref), so
  _default_/key is a valid explicit-default ref; prepending a slash would
  corrupt it. Applies to both asset and script_trigger via the shared cache.

join_pending_inputs (transient AND-join state) and materialized_asset_schema
(ducklake-only) are intentionally left alone; documented inline.

Verified end-to-end on a fresh DB: seed prior-state rows as the pre-fix parser
would have persisted them for data pipelines and scripts, run the full
migration suite, assert every row matches the fixed-parser identity (default
repair, hive/root/nested keys, named-storage + _default_ untouched, pre-cutoff
untouched, non-s3 untouched, duplicate collapse) across both tables.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-07-21 17:03:42 +00:00
Diego Imbert 7fb8a2e390 fix(parsers): keep s3 asset path suffix verbatim to preserve storage distinction (#10241)
* fix(parsers): keep s3 asset path suffix verbatim to preserve storage distinction

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01An2pTqSmqJd2XwnagvX4kM

* package json

* fix(pipelines): preserve named storage in generated TS/Python S3 URIs

The TS/Python templates emitted `s3:///${s3Key(path)}`, stripping the
leading slash and pinning the URI to default storage. For a named-storage
asset path (`secondary/key`) that produced `s3:///secondary/key`, which
resolves to the default storage with key `secondary/key`, dropping the
named-storage dependency and reading/writing the wrong object.

Emit the path verbatim after `s3://` (matching the DuckDB template) so a
named-storage input/output keeps its storage; identical to the previous
output for default-storage paths. Removes the now-unused `s3Key` helper.

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

* chore(cli): align bun.lock parser versions with frontend

The PR bumped windmill-parser-wasm-asset (1.749.0→1.753.0) and
windmill-parser-wasm-regex (1.692.0→1.764.0) in package.json and the npm
package-lock.json for both cli and frontend, but cli/bun.lock was left
pinned to the old versions. Sync it so the CLI's wasm asset parser (used
by localGraph inference) matches the frontend and deploy-time parser.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-07-21 16:25:06 +00:00
Ruben Fiszel dc5b006e7f fix npm checks 2026-07-21 15:59:26 +00:00
Ruben Fiszel 2caee41fdf fix(worker): mount /dev/shm as tmpfs in the Docker v2 nsjail sandbox (#10240)
The Docker v2 nsjail profile provided /dev/null, /dev/zero, /dev/random,
and /dev/urandom but omitted /dev/shm, since generate_rootfs_mounts()
skips the image's own /dev in favor of the profile's device nodes. Any
program needing POSIX shared memory (Ansible/Python multiprocessing,
Chromium) failed with "No such file or directory: /dev/shm".

Add a /dev/shm tmpfs mount, matching run.ansible.config.proto and
run.python3.config.proto.

Fixes WIN-2216

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:49:29 +00:00
Ruben Fiszel 68debab877 feat(triggers): add AMQP (RabbitMQ) trigger via lapin (#10230)
* feat(triggers): add AMQP (RabbitMQ) trigger using the lapin library

Fixes WIN-2214

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

* chore(triggers): defer AMQP cross-workspace deploy pending utils-internal publish

Revert the amqp_trigger additions to the shared windmill-utils-internal
TriggerDeployKind and the frontend cross-workspace deploy adapter: the
frontend installs the published npm package, which lacks the new kind
until a release is cut. AMQP create/edit/delete/list/sync/capture are
unaffected (they use local types); only cross-workspace deploy/merge of
AMQP triggers waits on the package bump. Also document the at-most-once
ack in the consumer loop.

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

* fix(triggers): address AMQP review — at-least-once ack, workspace cascade, contracts

- ack AMQP deliveries only after successful dispatch; nack+requeue on failure
- add ON DELETE CASCADE workspace FK so amqp_trigger rows are cleaned on
  workspace deletion (and the listener stops)
- fix the /amqp_triggers/test OpenAPI body and add amqp_trigger to
  WorkspaceDiffRow.kind
- register AMQP in the generated workspace trigger tool (create_trigger)
- drop banned $bindable defaults on optional props in the config section
- add build_uri unit tests (encoding, ports, vhost)

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

* fix(triggers): stop AMQP poison-message loop and reconnect on transient drops

Chaos testing against a live RabbitMQ broker showed the previous
nack(requeue) + immediate re-poll spun a tight redelivery loop (~1000
critical-error reports/sec) on a poison message, and any connection blip
permanently disabled the trigger (lapin has no built-in reconnect).

- on dispatch failure: nack+requeue then stop consuming; the listener
  framework re-lists the trigger after its ping goes stale (~15s), backing
  redelivery off to that cadence instead of a tight loop (verified: rate
  dropped from ~1000/s to ~1 per ~26s, message preserved)
- on connection/stream error: stop and let the framework reconnect instead
  of disabling; persistent failures are still disabled via get_consumer
  (verified: a forced connection close now auto-reconnects and resumes)
- finish the AI create-trigger action wiring for AMQP: add amqp to
  CreatedResourceTriggerKind, the action-card registry, and the drawer
  registry so the result card renders and its "Open" action works

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

* fix(triggers): complete AMQP frontend registries and defer merge rows

- add amqp to capturableTriggerTypes (so AmqpCapture mounts), the Runs
  jobTriggerKinds filter, and CLOUD_DISABLED_TRIGGER_TYPES
- wire AMQP into global AI chat mode: TRIGGER_KINDS, the request union,
  writeTriggerSchema, triggerServices, and the draft adapter
- stop emitting actionable AMQP fork-comparison rows (revert amqp_trigger
  from TRIGGER_OR_SCHEDULE_TABLES) since cross-workspace deploy is deferred
  until windmill-utils-internal is published — avoids a deploy that fails
  with "Unknown kind: amqp_trigger"
- use design-system TextInput instead of raw <input> in the config section

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

* fix(triggers): complete AMQP session/draft registries and constrain prefetch

- add amqp to the session-deploy, draft-compare, preview-router, and
  copilot workspace-item registries so AMQP drafts/deploys/nav/path
  resolution work
- include amqp_count in the MoveDrawer attached-trigger rename warning
- replace the raw prefetch <input> with a design-system TextInput bounded
  to an integer 1-65535 (backend u16) and block save on invalid values

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

* fix(triggers): make AMQP disconnect/reconnect consistent with the Kafka trigger

lapin, like rdkafka, has no transparent reconnect, so the AMQP listener now
mirrors the Kafka trigger's explicit reconnect loop instead of relying on the
framework re-list (which disabled the trigger once get_consumer failed on a
sustained outage):

- get_consumer returns cheaply; consume owns a (re)connect loop that retries
  with a 30s backoff, reports a critical error every 10 failed attempts, and
  reports a recovered critical error once it reconnects — never disabling the
  trigger on a connectivity failure
- a consumer/stream error breaks out to reconnect rather than disabling
- dispatch failure still nacks+requeues (at-least-once) with a short backoff
  to avoid a tight poison-message loop, keeping the connection alive

Verified against a live RabbitMQ broker: killing the broker keeps the trigger
enabled and retrying (attempt N), and restarting it auto-reconnects (logs
"reconnected after N attempts") and resumes dispatch.

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

* fix(triggers): complete AMQP capture registries and constrain prefetch contract

- add the 'amqp' case to triggerKindToTriggerType so opening the AMQP editor
  from a capture button no longer throws "Unknown TriggerKind: amqp"
- register AmqpIcon in CaptureTable's icon map and add an AMQP entry to the
  script/flow CaptureButton menu
- bound the OpenAPI prefetch_count to an integer 1-65535 (matches the Rust
  u16) and regenerate clients/prompts
- require a non-empty exchange name when the exchange binding is enabled
- build_uri: fall back to "/" on a blank vhost and bracket IPv6 hosts (+ tests)

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

* feat(triggers): wire AMQP into pipeline graph, git-sync, and preprocessor types

- asset_graph: discover attached amqp_trigger rows and emit an AMQP TriggerEdge
  so AMQP triggers render (and can be opened/deleted) on the data-pipeline canvas
- frontend pipeline graph: add amqp to NativeTriggerKind, the add-trigger menu,
  node presentation, event-trigger set, annotation keywords, and the
  editor/service registrations
- git-sync: add the amqp_trigger include pattern (+ test) so an AMQP git-sync
  deployment stages only its .amqp_trigger.* file, not an unrelated same-path object
- preprocessor starters: add the AMQP event to the generated TS/Python/PHP
  trigger event types (kind/payload/exchange/routing_key/queue_name/redelivered/
  delivery_tag)

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

* fix(triggers): finish AMQP pipeline/parser wiring, prefetch validation, source lists

- fix a stray edit that corrupted the pre-existing MqttTriggerEditor import
  ($lib/... path) in PipelineTriggerEditors.svelte
- reject prefetch_count = 0 server-side in validate_config (RabbitMQ treats 0
  as unlimited) and defensively skip basic_qos(0) in build_consumer (covers
  the capture path that bypasses CRUD validation)
- recognize `// on amqp` in the canonical parser (TriggerSpec::Amqp) and add
  amqp to the CLI non-autorun/event-trigger sets so a pipeline cascade never
  runs an AMQP-only node as a manual root without an event
- add amqp to the preprocessor intro lists and both pipeline AI instructions

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

* fix(triggers): reject zero AMQP prefetch in all paths and finish guidance lists

- extract a shared validate_amqp_options used by both CRUD validate_config
  and build_consumer, so capture configs (which bypass CRUD validation) also
  reject prefetch 0 instead of silently connecting with an unlimited buffer
  (+ unit tests for 0/1/65535/None)
- add AMQP to the main script-writing preprocessor-sources prompt and the CLI
  triggers-skill guidance list

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

* docs(triggers): de-duplicate AMQP prefetch comment and fix GET response text

- keep the zero-prefetch rationale only on the shared validate_amqp_options
  doc; drop the redundant call-site comments
- correct the getAmqpTrigger OpenAPI 200 description ("deleted" -> "retrieved")

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

* chore: update ee-repo-ref to aaa6cb89b05b76139252c64f057e53b94d12ac60

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

Previous ee-repo-ref: 5da5fd65aca9594b2611837a52e4677b544b0380

New ee-repo-ref: aaa6cb89b05b76139252c64f057e53b94d12ac60

Automated by sync-ee-ref workflow.

* chore(migrations): consolidate the four AMQP migrations into one

The table and the three enum ADD VALUE statements (trigger_kind, job_trigger_kind,
draft_kind) are one atomic feature. ALTER TYPE ... ADD VALUE runs inside the
migration transaction on PG >= 14 (Windmill's minimum) since the amqp_trigger
table doesn't reference those enum types, so they can share a single migration
instead of four. Verified applying cleanly in a single transaction on a fresh DB.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-07-21 15:10:29 +00:00
hugocasa c7ee192680 update ee ref to git sync cli prompt fix (#10224) 2026-07-21 14:37:38 +00:00
Ruben Fiszel c6fd6da971 repoint slack hubPaths to form-data 4.0.6 versions (#10239)
The image prebundles the hub scripts listed in hubPaths.json by their
pinned (immutable) version. windmill-integrations republished the four
image-cached slack scripts with form-data bumped 4.0.5 -> 4.0.6; point
hubPaths.json at the new versions so freshly built images cache the fixed
lock instead of the stale one.

Fixes part of #10219

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:37:10 +00:00
Diego Imbert d24e176816 fix(parser): spurious pg arg inferred from placeholders in comments (#10226)
* fix(parser): ignore pg placeholders in comments, strings and dollar quotes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NXWRDKzswMeDBP5THXpb9Q

* package json + lock

* chore: remove stray root npm lockfiles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kuUhCPD9ph4ZuFd13SCWJ

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:36:44 +00:00
Diego Imbert b0bf25683b fix(frontend): prevent browser back-swipe navigation over monaco editors (#10229)
* fix(frontend): prevent browser back-swipe navigation over monaco editors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEP1T4UogYABBifdNJUUuQ

* fix: toggle root overscroll-behavior-x on editor hover instead of cancelling wheel events

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEP1T4UogYABBifdNJUUuQ

* refactor: replace per-editor swipe guard action with single global handler in root layout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEP1T4UogYABBifdNJUUuQ

* style: compress swipe-guard comment to four lines

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q97WNWUT9mntEEVhg6kpzv

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:36:33 +00:00
Diego Imbert 32994df427 fix: return to parent workspace when a fork is deleted remotely (#9898)
* fix: return to parent workspace when a fork is deleted remotely

When a workspace fork was deleted remotely while a user had it open,
reloading stranded them: a regular member got logged out (whoami fails on
the vanished workspace) and a superadmin silently landed on a dead
workspace whose requests 404. Detect the deleted fork on load and redirect
to its parent (or the workspace picker) with a toast instead.

- forkParentMemory.ts: persist a bounded fork->parent map in localStorage
  while a fork is reachable (the parent is unrecoverable post-deletion).
- (logged) layout: record the current fork's parent via an effect.
- root layout: tryRecoverFromDeletedFork detects the vanished fork in
  loadUser and redirects to the remembered parent or the workspace picker,
  reusing the workspace list already fetched on mount.

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

* fix: verify fork existence for superadmins before deleted-fork redirect

* fix: only recover deleted fork on actual 404 from superadmin check

* fix: recover prefixless dev-workspace forks via remembered parent

* fix: use workspace exists check to detect deleted forks

* fix: restore wm-fork- detection for non-member superadmin forks

* fix: record fork parent for non-member superadmin dev workspaces

* Update frontend/src/routes/(root)/+layout.svelte

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix: store fork parent map with null-prototype to handle __proto__ ids

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-07-21 14:35:59 +00:00
Guilhem 9bc1f62128 feat(ai): session chat nits — empty sends, command picker polish, session-state prompt (#10233)
* feat(ai): session chat nits: empty sends, picker polish, session state

* fix(ai): scope empty-send turns to global chat and pin new behavior

* fix(ai): align grouped search nav with display order, enable empty-send button

* fix(ai): fork fallback for unlisted workspaces, section headers across branches

* style(ai): hint-colored 3xs picker section headers, drop inline row descriptions

* style(ai): more spacing between picker sections

* fix(ai): require context elements for empty global-chat sends

* style(ai): no empty bubble for text-free messages

* fix(ai): review round 2 — keyboard tooltip access, requestedMode guard, no display names in prompt

* fix(ai): queue context-only drafts pressed while a response streams

* fix(ai): shared context identity for queued badges and full queue-context union
2026-07-21 14:01:44 +00:00
Ruben Fiszel 39b9a9d3d5 bump slim image Python to 3.12.13 to ship current pip (#10236)
* fix(docker): upgrade bundled pip in uv-managed Python runtimes

uv-managed Python runtimes ship a pip that lags behind, and it gets baked
into the full, slim, and EE slim images at build time. Add a pinned
PIP_VERSION build arg and upgrade pip in every real managed runtime after
`uv python install`, failing the build if any upgrade fails.

Fixes part of #10219

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

* fix(docker): simplify pip upgrade to native uv per-version install

Replace the runtime-discovery loop with a single
`uv pip install --python <ver> --system --break-system-packages --upgrade`
per known Python version. uv resolves the managed interpreter by version,
so no directory scan or symlink handling is needed; a plain RUN still fails
the build if the pinned pip can't be installed.

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

* fix(docker): bump slim image Python patch to 3.12.13 for current pip

The full image floats LATEST_STABLE_PY to 3.12 (→ 3.12.13), whose
uv/python-build-standalone runtime already ships pip 26.1.2. The slim and
EE-slim images pinned 3.12.12, which ships pip 26.0.1 — the only runtime
actually behind. Align them to 3.12.13 so they inherit the current pip
natively, instead of adding an explicit pip-upgrade build step.

This supersedes the earlier pip-upgrade approach on this branch: the full
image needed no change, and the slim gap was purely the stale patch pin.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:38:21 +00:00
Guilhem 572d69e5ae feat(ai): open the Compare & Deploy page from chat with item preselection (#10232)
* feat(ai): open the Compare & Deploy page from chat with item preselection

* fix(ai): label the compare link card outside sessions

* fix(ai): scope untracked-chat compare links to explicit items

* style: drop narration comment on compare mask precedence

* fix(ai): match compare items mask against parked live-draft paths

* fix(ai): land maskless-mode compare on the view holding the masked drafts

* fix(ai): honor explicit fork mode over the draft-mask heuristic

* docs(ai): describe mask-aware compare mode auto-pick

* fix(ai): match legacy app fork diffs under their identity mask key
2026-07-21 12:58:18 +00:00
Ruben Fiszel 28966bdbf1 fix(frontend): curl fallback for +Variable/+Resource in bash sandbox mode (#10235)
* fix(frontend): use curl fallback for +Variable/+Resource in bash sandbox mode

When a bash script uses `# sandbox <image>` or `# docker`, the body runs
inside a custom container image that does not have the `wmill` CLI
installed, so the `wmill variable get` / `wmill resource get` snippets
inserted by the +Variable and +Resource pickers fail.

Detect `# sandbox`/`# docker` in the editor code and insert a curl-based
snippet using the BASE_INTERNAL_URL, WM_TOKEN and WM_WORKSPACE env vars
(available in sandbox) instead.

Fixes WIN-2215

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

* fix(frontend): mirror worker grammar and use curl/wget fallback in sandbox mode

Address review of the bash sandbox picker fallback:

- Extract detection into `bashRunsInCustomImage`, mirroring the worker's
  BashAnnotations grammar (leading comment lines only; `# sandbox <image>`
  or bare `# docker`). A bare `# sandbox` is the nsjail-bash modifier that
  still runs on the worker rootfs where `wmill` is available, so it now
  correctly keeps the `wmill` snippet. This also fixes the substring
  false positives (`# sandboxed`, `# docker` in prose/body) and false
  negatives (`#sandbox <image>`).
- The default `# sandbox alpine:latest` image ships busybox `wget`, not
  `curl`, so the snippet now tries `curl` then falls back to `wget`.
- `variables/get_value` returns a JSON-quoted string; strip the outer
  quotes with `sed` so the sandbox snippet matches the `jq -r .value`
  output of the non-sandbox branch. Resources return JSON either way.
- Add focused unit tests for the detection grammar.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:53:52 +00:00
Ruben Fiszel 6e42633643 fix(pg): actionable error when s3object input exceeds jsonb 256MB cap (#10228)
Native SQL PostgreSQL scripts with an `(s3object)` input materialize the
whole referenced file into a single jsonb parameter. PostgreSQL hard-caps a
jsonb value's element payload at 256MB, so a large file fails with an opaque
`total size of jsonb array elements exceeds the maximum of 268435455 bytes`.

`materialize_s3object_args` now reports the largest materialized payload, and
that specific server error is rewritten into guidance explaining the input is
materialized (not streamed) and pointing large-file users at DuckDB, which
reads S3 natively and streams.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:21:34 +00:00
Ruben Fiszel 39058c0a01 fix(postgres-triggers): enforce resource-path scopes on ancillary routes (#10222)
* fix(postgres-triggers): enforce resource-path scopes on slot/publication routes

The Postgres trigger ancillary routes (slot management, publication
management, version/logical-level checks, template script creation) relied
only on the route-level middleware, which validates the scope domain+action
but delegates resource-path enforcement to each handler. These handlers made
no check_scopes call, so a token scoped to one postgres resource path (e.g.
postgres_triggers:write:u/alice/*) could drive these endpoints against any
postgres resource in the workspace, including the destructive drop_slot_name
(pg_terminate_backend + pg_drop_replication_slot).

Add a check_scopes call at the top of each affected handler, before any
connection is opened, mirroring the generic trigger CRUD handlers. Read
endpoints require postgres_triggers:read:{path}; write endpoints require
postgres_triggers:write:{path}.

Fixes WIN-2213

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

* docs: condense postgres_trigger_scope test module comment

Address Codex nit: state the durable constraint (mismatched scope must fail
before DB access) instead of narrating pre-fix behavior and change history.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 09:21:07 +00:00
Ruben Fiszel fd51d40f12 feat(pipelines): catalog declared measures and dimensions (#10190) 2026-07-21 07:34:44 +02:00
hugocasa adc555d172 fix(triggers): apply scope-path filtering to list and fix update scope check (#10220)
The shared `list_triggers<T>` handler returned every trigger row of its
type in the workspace regardless of the token's declared scope. A token
limited to e.g. `http_triggers:read:<prefix>/*` could enumerate all
trigger paths (and their configs) through the `/list` endpoint, while
`get_trigger`, `create_trigger`, `delete_trigger` and `exists_trigger`
correctly rejected them. This affected all 10 TriggerCrud kinds (HTTP,
WebSocket, Kafka, NATS, MQTT, SQS, GCP, Azure, Postgres, Email).

Apply `build_scope_path_predicate(&authed, T::scope_domain_name(),
"read")` to the returned rows after the draft-only append, mirroring
scripts, flows, apps, resources, variables and schedules. A `HasPath`
supertrait on `Self::Trigger` exposes the row path to the shared handler
without each impl restating it (`Trigger<T>` returns `&base.path`, the
`()` OSS stub returns "").

Also fix `update_trigger`: it only checked scope against the new path in
the request body, letting a scoped token move a trigger it can't touch
into its scope. Now check both the existing path (URL) and the new path.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 07:09:31 +02:00
Ruben Fiszel 7ec56ef619 fix: require auth on GET /concurrency_groups/{job_id}/key (#10221)
The /concurrency_groups global service is registered after the router's
.route_layer(from_extractor::<ApiAuthed>()), and Axum's route_layer only
wraps routes present at the call site, so this handler was left with no
router-level auth. Unlike its siblings list_concurrency_groups and
prune_concurrency_group, get_concurrency_key declared no ApiAuthed
parameter, so GET /api/concurrency_groups/{job_id}/key was reachable
unauthenticated and would return concurrency keys (which can embed
workspace ids, script/flow paths, and $args-templated argument values)
for arbitrary jobs instance-wide.

Add _authed: ApiAuthed so the extractor independently enforces
authentication regardless of layer placement, matching the
defense-in-depth pattern of the sibling handlers.

Fixes WIN-2212

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 07:08:16 +02:00
Ruben Fiszel 858d9a5527 chore(main): release 1.764.0 (#10196)
* chore(main): release 1.764.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
v1.764.0
2026-07-20 22:56:04 +02:00
Ruben Fiszel ddec2abbb3 feat(jobs): cap total queued jobs per workspace on cloud (#10218)
* feat(jobs): cap total queued jobs per workspace on cloud

A workspace could flood the queue with an unbounded number of jobs across
many concurrency keys and scripts (or keyless jobs), which the per-key
cap from #10197 does not bound. Add a companion instance-wide ceiling on
a workspace's total queued jobs.

check_workspace_queue_cap rejects a push once the workspace has
WORKSPACE_MAX_QUEUED_JOBS (default 20000, superadmin-configurable, 0 to
disable) jobs queued, cloud-only and runtime-gated on CLOUD_HOSTED like
the per-key cap. It runs on every push, so it applies even to premium
workspaces and catches parallel for-loop floods. Jobs already queued
still drain; only new pushes past the ceiling are rejected, so an
in-flight flow only fails to push further work while at the ceiling.

The setting loader self-gates on CLOUD_HOSTED so it is never loaded off
cloud, from initial load or a settings-change reload. The depth count is
bounded by the cap via LIMIT so a runaway backlog never costs an
unbounded scan on the push path.

* docs(jobs): note the workspace cap is a soft ceiling and the depth helper is count-only

Records the two review points as constraints: the cap does not serialize
admission (a soft ceiling by design, like the per-key cap), and
workspace_queue_depth is pub only for the test, returns a count not job
data, and leaves authorization to the caller.
2026-07-20 22:09:59 +02:00
hugocasa b070f56c5e feat: detect server-handled git-sync so CLI picks git push vs wmill sync push (#10201)
* feat: detect server-handled git-sync so CLI picks git push vs wmill sync push

Add a non-admin GET /w/{w}/workspaces/git_sync_deploy_mode endpoint returning
{configured, deploy_on_push}, so any workspace member (not just admins, who
alone can read get_settings) can tell whether pushing to the git remote deploys
via server-side auto-pull. Surface it through `wmill gitsync-settings status`
and align the deploy guidance/skills to prefer git push when the repo deploys on
push, falling back to `wmill sync push` otherwise.

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

* fix: address review — clean JSON output, warn on git_sync parse failure

- gitsync-settings status --json-output now uses console.log so the JSON pipes
  cleanly to jq (log.info wraps it in ANSI color codes)
- get_git_sync_deploy_mode logs a warning on git_sync deserialize failure instead
  of silently reporting configured=false, and documents why it is not EE-gated

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

* fix: address codex review — license-gate and branch-match deploy-on-push

- get_git_sync_deploy_mode now reports deploy_on_push only on Enterprise-licensed
  instances (auto-pull can't run on CE/downgrade) and returns auto_pull_branches
  so the client knows which tracked branches actually deploy on push
- gitsync-settings status matches the local git branch against auto_pull_branches
  before recommending git push, so an untracked branch falls back to wmill sync push
- add an integration assertion for the endpoint's default (no git-sync) shape

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

* docs: point per-topic skills at the canonical Deploying section

The git-push-vs-wmill-sync-push decision lives in core.ts (AGENTS.wmill.md),
which is already in context. Have the per-topic skills reference the Deploying
section instead of re-encoding the detection, so there is one source of truth
and no drift (the compressed version also wrongly implied `gitsync-settings
status` detects the CI-workflow path, which only core.ts's filesystem check does).

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

* fix: match remote+branch server-side for deploy-on-push detection

Codex flagged that a workspace-level auto-pull signal recommends `git push` even
when the local checkout is a different repo/branch than the one that auto-deploys.
Match precisely instead, without exposing anything sensitive:

- git_sync_deploy_mode takes optional remote+branch query params. The backend
  normalizes each auto-pull repo's URL to host/path (dropping embedded
  user:token credentials by rebuilding from parsed components, never scrubbing
  the string) and compares to the caller's remote; deploy_on_push is true only on
  a licensed instance where an auto-pull repo matches that remote and tracked
  branch. The response is two booleans — no repo URLs or branches leave the server.
- Branchless (default-branch) and fork/sync_forks repos stay a safe fallback to
  `wmill sync push` rather than a wrong git-push recommendation.
- CLI status sends `git remote get-url` + current branch (new getGitRemoteUrl
  helper, --remote flag) and reports the matched result.
- Unit-test the URL normalization/credential-stripping directly, since a
  regression there would be a token-handling bug.

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

* fix: address codex security findings in deploy-mode detection

- Strip credentials from the local remote client-side before sending, so a
  token embedded in the URL never reaches the server's request-URI logs
- Fetch the remote via spawnSync arg array (not an interpolated shell string),
  removing a command-injection path from a caller-supplied --remote value
- Use the remote's push URL (`git remote get-url --push`) and recommend the
  qualified `git push <remote> <branch>`, so the pushed target matches the one
  the server checked
- Keep the port in remote normalization so different services on the same host
  don't collide into a false match
- Unit-test credential stripping (CLI) and port distinctness (backend)

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

* feat: resolve $var repos and fork sync_forks in deploy-mode detection

- Interpolate $var:/$res: references in the repo url/branch the same way the
  auto-pull poller does (system context, cached, only when a field is a
  reference), so variable-backed git URLs match instead of falling through
- For a fork workspace, evaluate the root ancestor's git-sync settings and treat
  its wm-fork/<base>/<id> branch as deploying when the root repo has
  auto_pull.enabled && sync_forks and its base matches the tracked branch
- Read settings/resources on the plain pool (a fork member may not belong to the
  root workspace); only booleans are returned
- Unit-test the fork/branch matching (base + sync_forks + workspace-id suffix)

A blank tracked branch (repo default) still needs a network ls-remote to resolve,
so it stays a safe fallback to `wmill sync push` rather than a wrong git push.

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

* refactor: share one git-repo resolver between poller and deploy-mode

The auto-pull poller and the deploy-mode endpoint both resolved a git-sync repo
resource (system context, $var:/$res: interpolation) with duplicated boilerplate.
Extract windmill_store::resources::resolve_git_repository_resource and have both
call it, so the interpolation lives in one place. Drops the endpoint's local
resolve_repo_url_branch helper and its raw SQL query (and cache entry).

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

* fix: address codex review — fork false-positives, shell-safety, auth contract

- Fork deploy detection now mirrors reconcile_fork_branch_pull: the wm-fork branch
  must route to this workspace (first existing of the id candidates) and the repo
  must be in the fork's own inherited settings, so a multi-repo root or an
  ambiguous id can't produce a false deploy_on_push
- Recommended deploy command is shell-quoted (branch/remote names may contain
  metacharacters and the output is agent-executed)
- Remote normalization folds only the host; repo paths stay case-sensitive
- Document the system/RLS-bypassing contract on the shared resolve helper and
  restore the head-fetch doc; fix the overclaiming integration-test comment
- Dev-workspace label and default-branch cases remain documented safe fallbacks

Also restores 5 sqlx cache entries an earlier cleanup dropped.

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

* fix: require a runnable auto-pull delivery path for deploy_on_push

enabled auto-pull alone doesn't mean a push deploys: a webhook-only repo with no
active hook (failed registration), or a repo that only polling could serve on an
SSH URL (the poller rejects SSH), delivers nothing. Gate deploy_on_push on an
actual delivery path — active webhook, or a pollable non-app HTTPS repo — per the
repo's auto-pull mode. Unit-tested across modes/webhook/URL-scheme/app.

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

* fix: read fresh repo config for on-demand deploy-mode detection

resolve_git_repository_resource took an implicit allow_cache=true (right for the
poller loop). An on-demand status could then match against a stale url/branch
cached by an earlier poll. Make allow_cache a parameter: poller keeps true, the
deploy-mode endpoint passes false so it reflects the current git-sync config.

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

* fix: defer the deploy fallback instead of assuming wmill sync push

When backend auto-pull doesn't match the checkout, `status` no longer flatly
recommends `wmill sync push` — a CI workflow may still deploy on push. It now
reports the backend signal and points at the Deploying guidance (check CI → git
push, else wmill sync push; record the choice as a `Deploy mode:` line in
AGENTS.md). deploy_command is null in JSON when undetermined. This resolves the
CI-backed false recommendation without the CLI re-implementing CI detection.

Also fix two review nits: restore the deploys_on_push_branch doc comment (it had
drifted onto has_runnable_delivery) and correct the app-repo comment (their
exclusion from the poll path is a conservative safe under-report, not "can't be
polled").

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

* refactor: drop the ambiguous-fork-id disambiguation from deploy-mode

The existence-query resolution guarded a very narrow case (a suffix owned by both
a coexisting wm-fork-<suffix> and <suffix> workspace, queried from the wrong one).
Not worth the per-fork query; keep the cheap candidate-family check plus the
inherited-repo membership test, which already close the real fork false-positive.

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

* refactor: drop remote-URL matching; disambiguate deploy-mode by repo count

Matching the caller's git remote URL against each repo dragged in the whole
remote-URL surface (sending the URL, credential stripping, shell-safe remote
handling, fetch/push URL, port/case normalization) — and the risk that came with
it. Replace it with a simpler rule that fits the actual question:

- deploy_on_push is true only when exactly ONE licensed, deliverable auto-pull
  repo tracks the pushed branch. With a single synced repo the local checkout is
  unambiguously it; with several we can't tell which is the caller's, so we
  return false and the CLI asks the user.
- The endpoint takes only `branch` (no `remote`); status no longer reads or
  sends the git remote.
- On the fallback, status now tells the agent to ASK the user how the repo
  deploys (CI git-push vs wmill sync push) and record it in AGENTS.md, instead of
  assuming wmill sync push. Guidance updated to match.

Removes normalize_git_remote (+url dep), getGitRemoteUrl, stripGitRemoteCredentials,
shellQuote, the --remote flag, and their tests.

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

* refactor: share fork-branch routing between reconciler and deploy-mode

Deploy-mode detection was re-deriving fork/dev routing (root walk, wm-fork/dev
branch parsing, descendant resolution, inherited-repo check) that the auto-pull
reconciler already owns — the source of repeated edge-case bugs. Extract it into
windmill_common::workspaces::resolve_fork_branch_target and have both the endpoint
and reconcile_fork_branch_pull (EE) call it, so they can't drift and dev
workspaces are handled by construction.

Endpoint now resolves the root via the canonical cached fork_ancestor_chain
(dropping a duplicate CTE) and routes forks/dev workspaces through the shared
resolver. The .sqlx cache is unchanged (the moved queries already existed).

Bumps ee-repo-ref for windmill-labs/windmill-ee-private companion.

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

* fix: exclude archived roots and frame deploy_on_push:false as unconfirmed

- deploy_on_push now requires the root workspace to be live; polling and webhook
  delivery both exclude deleted roots, so an archived root (or anything beneath
  one) with retained git-sync no longer reports deployable
- status and the OpenAPI now describe false as "not confirmed" (it also covers
  ambiguity and conservative false-negatives), not a definite no — the CLI asks
  the user rather than asserting the push won't deploy

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

* chore: bump ee-repo-ref for EE branch merge of main

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:44:24 +02:00
hugocasa 11fda89b52 feat(telemetry): generic feature-usage telemetry with AI session metrics (#10200)
* feat(telemetry): add generic feature_usage table and batched logging endpoint

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(telemetry): log AI session usage events and document them in telemetry settings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): use escape sequence instead of literal NUL bytes in buffer key

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): validate dimensions, decouple retention, keepalive flush

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): allowlist feature-usage dimensions and index retention scans

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): pin tool-name allowlist and deploy session attribution

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(telemetry): route AI chat usage through feature_usage and drop ai_chat_usage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(telemetry): slim dimension validation to registered kinds plus key shape

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): backfill ai_chat_usage into feature_usage before dropping it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): disclose provider and model identifiers in telemetry settings text

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): issue all flush chunks before awaiting so pagehide keeps them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: update ee-repo-ref to 6306c072a50937ea9af44a5bcf42345543207486

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

Previous ee-repo-ref: 964f242a0eb44db7f7d26636cc8d76aeabea2b73

New ee-repo-ref: 6306c072a50937ea9af44a5bcf42345543207486

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-07-20 20:56:57 +02:00
windmill-internal-app[bot] f635bd5ae7 chore: refresh vendored docs snapshot (#10068)
Co-authored-by: hugocasa <15649739+hugocasa@users.noreply.github.com>
2026-07-20 20:43:03 +02:00
Ruben Fiszel 11bb37d7ca fix(forks): show workspace settings link in sidebar for fork creators (#10216)
* fix(forks): show workspace settings link in sidebar for fork creators

PR #10166 grants a fork's creator developer-management access to the
workspace settings page, but every sidebar entry point to
/workspace_settings stayed gated on is_admin || superadmin, so a
non-admin fork creator could only reach the page by typing the URL.

Widen the four sidebar guards (SettingsMenu, SidebarContent,
WorkspaceMenu, WorkspaceScopeHeader) to also admit fork owners, via a
shared isForkOwner() helper mirroring the fork-owner check already on
the settings page and the backend authorize_fork_owner_add_user grant.

Fixes WIN-2210

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

* fix(forks): check fork ownership against the settings entry's target workspace

In session mode SettingsMenu's entry targets workspaceSettingsTarget,
which can differ from the active workspace, but the fork-owner guard was
checked against the active workspace — hiding the entry when the session
targets the user's fork, or exposing a dead link when only the active
workspace is theirs. Check ownership against the workspace the entry
actually points at.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 20:39:48 +02:00
hugocasa 87be041c09 fix(git-sync): avoid percent-encoded colon in git-sync hub script path (#10213)
* fix(git-sync): avoid percent-encoded colon in git-sync hub script path

The git-sync init/detection hub script slug contained a colon stored as
`%3A` in the run-by-path URL. The generated API client re-encodes path
params with encodeURI, turning `%3A` into `%253A` (double-encoding). Some
hardened reverse proxies / WAFs reject double URL-encoding and return a
bare 400 before the request reaches Windmill, breaking git-sync repository
detection on those instances.

The hub resolves scripts by numeric id and ignores the slug, so dropping
the colon from the slug is behavior-neutral (same script, same id-keyed
worker cache) while producing a colon-free run URL.

Also force-cache GIT_SYNC_PULL_SCRIPT_PATH at build alongside
LATEST_GIT_SYNC_SCRIPT_PATH so the backend-driven pull script is always
baked into the image for airgapped workers, instead of relying on an
incidental hubPaths.json overlap.

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

* chore: bump ee-repo-ref for git-init slug match fix

Pulls in windmill-ee-private#676 so the EE is_git_init_script check matches
the colon-free git-init hub slug (GitHub App token grant for git-sync jobs).

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

* chore: bump ee-repo-ref for git-init slug helper + test

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

* chore: update ee-repo-ref to a3adea1ffb406e709cc480871df58fab6c51aca1

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

Previous ee-repo-ref: cef4e008ef62dec434aa9bb3ec783db8aff6a1c1

New ee-repo-ref: a3adea1ffb406e709cc480871df58fab6c51aca1

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-07-20 20:36:34 +02:00
Guilhem 0593ff7d7d feat(ai): add npm package search tool to global chat (#10204)
* feat(ai): add npm package search tool to global chat

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ai): encode npm search query before building registry URL

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(ai-evals): add global-mode npm package search case

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-07-20 18:28:09 +00:00
Guilhem c8870d36ae fix(sessions): session bar badge readouts, job persistence, refresh bounce (#10217) 2026-07-20 18:27:56 +00:00
hugocasa 2b58df57fc feat(ai): extract prompt cache token usage from OpenAI and Azure providers (#10214)
* feat(ai): extract prompt cache token usage from OpenAI and Azure providers

Parse the nested cache token details OpenAI returns and thread them into
TokenUsage, matching the Anthropic and Bedrock providers.

- sse.rs: add OpenAIPromptTokensDetails / OpenAIInputTokensDetails and the
  optional prompt_tokens_details / input_tokens_details fields.
- other.rs (Chat Completions) and openai.rs (Responses): populate
  cache_read via .with_cache(cached_tokens, None).

OpenAI's prompt_tokens/input_tokens already include cached tokens (cached is
a subset), so total/prompt are unchanged; cache_read is recorded separately
for reporting. For the same reason the frontend token-usage conversions are
left as-is (adding cached would double-count); optional cache fields and a
clarifying comment are added to prevent a future incorrect Anthropic-style fix.

Fixes WIN-2207

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

* test(ai): pin OpenAI/Azure cache-token deserialization paths

Add regression tests deserializing the real Chat Completions and Responses
usage payloads, guarding the prompt_tokens_details.cached_tokens /
input_tokens_details.cached_tokens paths against a silent rename.

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

* refactor(ai): extract to_token_usage() and test the cache mapping

Address review nit: move the usage->TokenUsage conversion into
OpenAIChatUsage::to_token_usage / OpenAIResponsesUsage::to_token_usage so
the providers call one method and the tests exercise the real mapping.
Tests now assert cache_read is populated while input/total are unchanged.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:47:25 +02:00
hugocasa 0e04bc6991 fix: steer ai chat away from draft-blind api catalog reads and runs (#10202)
* fix: steer ai chat away from draft-blind api catalog reads and runs

* fix: support api catalog tools in ai eval harness mock backend

* fix: block remaining draft-blind read and list endpoints in api catalog

* fix: scope eval fetch stub to handled benchmark api paths

* feat: add deployed version read option to read_workspace_item

* fix: include input schema in script workspace item reads

* feat: add pagination to list_workspace_items

* fix: paginate list_workspace_items per item type without cross-type loss

* fix: window draft overlay in list_workspace_items by page and limit

* refactor: simplify list draft overlay to capped page-1 merge

* fix: label server-synthesized draft-only rows as drafts in listings

* chore: fix stale eval comment and pin draft_only listing label
2026-07-20 18:45:14 +02:00
Ruben Fiszel 38ceae1a09 fix(db): grant schema usage and re-run windmill role grants (#10212)
* fix(db): grant schema usage and re-run windmill role grants

Migration 20250205131523 grants the windmill_user and windmill_admin
roles access to the schema and its tables, but its first statement is
LOCK TABLE pg_catalog.pg_roles, which requires superuser. On managed
Postgres (RDS, Cloud SQL) the migration user is not one, the lock
raises, and the block's EXCEPTION WHEN OTHERS handler downgrades the
failure to a NOTICE, so every GRANT after it is skipped. Core tables end
up ungranted and queries on a user_db transaction (SET LOCAL ROLE
windmill_user/windmill_admin) fail with "permission denied for table"
or, when schema USAGE was never granted, "relation does not exist".

Add a migration that re-runs those grants without the lock and without a
catch-all handler, and add the missing GRANT USAGE ON SCHEMA public to
init-db-as-superuser.sql, which PostgreSQL 15+ no longer implies for
PUBLIC. The init script also now creates windmill_admin before the table
grants so role membership is in place when they run.

Fixes WIN-2208

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

* docs(db): correct schema-privilege rationale in grant migration

PostgreSQL 15 revoked CREATE, not USAGE, from PUBLIC on the public
schema, so USAGE is still granted by default there. The explicit grant
is what a hardened database that revoked it needs, not a PG15 default.
Also correct the description of what 20250205131523's failure actually
loses: 20221105003256 grants the tables outside any locked block, so the
gap is the ALTER DEFAULT PRIVILEGES covering later-created tables.

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

* fix(db): grant only runner-owned objects to avoid aborting upgrade

GRANT ALL ON ALL TABLES IN SCHEMA raises a hard "permission denied for
table X" the moment the schema holds an object the migration runner does
not own (a superuser-installed extension such as PostGIS spatial_ref_sys,
or a co-located application table). With no catch-all handler that would
abort the whole upgrade -- a regression against 20250205131523, which
tolerated it only by swallowing every error.

Grant per-object over just the tables and sequences the runner owns,
which is exactly the set the GRANT can succeed on. Windmill's own tables
are all runner-owned, so coverage is unchanged; foreign objects are
skipped rather than aborted on. ALTER DEFAULT PRIVILEGES stays (it only
governs the runner's future objects, so it cannot conflict).

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

* fix(db): guard each grant so the migration can never abort an upgrade

Wrap every grant in its own BEGIN/EXCEPTION/WARNING so no single failure
can abort the migration -- the opposite of 20250205131523's one
block-wide WHEN OTHERS, whose flaw was granularity: a single early
failure there silently skipped every remaining grant. Here each grant
that cannot be applied is isolated, re-raised as a named WARNING, and the
rest still run.

This closes the last abort paths: a grant on an object dropped by another
session between the catalog scan and the GRANT, USAGE on a schema the
runner cannot grant, or a missing role, now warn and continue instead of
failing the upgrade. On a clean owned schema the guards never fire (zero
warnings, verified). The owner filter stays so foreign objects are
skipped without even a warning; the guard is the backstop.

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

* fix(db): also grant runner-owned views and materialized views

pg_tables returns only ordinary and partitioned tables, so the previous
loop left owned views (flow_workspace_runnables and any materialized
views) ungranted -- a coverage regression against the GRANT ... ON ALL
TABLES form, which grants views too. Those views are read through
user_db transactions, so windmill_user/windmill_admin need access.

Enumerate pg_class over the relkinds ALL TABLES covers (r, p, v, m, f),
keeping the ownership filter so foreign objects are still skipped.

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

* fix(db): grant by effective authority, not owner-name equality

Filtering relations by pg_get_userbyid(relowner) = current_user skips
objects the runner can legally grant but does not own by name: after a
migration-credential rotation, tables and sequences stay owned by the
previous runner while the new runner grants through inherited membership
(or as a superuser). Owner-name equality would leave those ungranted and
their user_db access broken.

Filter by pg_has_role(current_user, owner, 'USAGE') instead -- objects
the runner owns directly, inherits ownership of, or reaches as superuser.
Genuinely foreign objects (owner the runner is not a member of) are still
skipped, so the per-object guards remain the backstop.

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

* fix(db): scope grants by explicit membership, not superuser authority

pg_has_role treats a superuser as a member of every role, so a superuser
migration runner would grant windmill_user/windmill_admin access to every
co-located relation in the schema -- another application's tables, an
extension's tables -- not just Windmill's.

Compute the runner's role set from pg_auth_members (recursive explicit
membership) and grant only relations owned by it. This still covers the
credential-rotation case owner-name equality missed (the new runner is a
real member of the previous owner) without inheriting the superuser's
implicit authority over unrelated roles.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:32:47 +02:00
Guilhem f4308cf033 feat(ai): expose get_db_schema tool in global chat (#10207)
* feat(ai): expose get_db_schema tool in global chat

* fix(ai): skip cross-workspace editor cache write in global get_db_schema

* test(ai): add global eval case for get_db_schema resource lookup
2026-07-20 17:32:59 +02:00
Guilhem 542a4842a3 feat(ai): live web-search source list on chat tool cards (#10210)
* feat(ai): live web-search source list on chat tool cards

* fix(ai): restrict web-search source links to http(s), respect user collapse

* fix(ai): read plural queries field for openai web-search labels
2026-07-20 15:47:04 +02:00
Guilhem b448af1da7 feat(sessions): live DOM access for the raw-app preview in AI sessions (#10129)
* feat(sessions): live DOM access for the raw-app preview in AI sessions

Give the session chat read-only access to the rendered raw-app preview,
plus inspector-picked element chips synced bidirectionally with the preview.

- search_dom / read_dom tools: live, same-origin contentDocument reads by CSS
  selector (selector omitted = whole body), reusing the file engine over
  pretty-printed outerHTML (worker-guarded, bounded). Session-gated.
- Inspector picks become app_dom_selector context chips (selector-only); the
  model fetches content on demand. Chips shown even in GLOBAL/session mode.
- Multi-select chips synced with the preview: the chip list is the source of
  truth, pushed to the harness which renders one highlight per selector; add,
  chip-remove, and preview-× remove all stay in sync. Overlays stripped from
  search_dom output.

The ui_builder harness changes (multi-select highlights + unique nth-of-type
selectors) live in windmill-code-ui-builder and ship via the artifact re-pin.

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

* feat(sessions): inline element prompt + inspector polish for raw-app DOM

Session preview:
- Add InlineElementPrompt: a floating mini-composer anchored top-left over a
  selected element; sends a chat turn scoped to that element (its chip is
  context). Autofocus + remount per selection, borderless input, label gutter
  so it clears the harness name+size pill.
- autosize action gains an optional minHeight (default 30) so the compact
  inline input renders a tight single line.
- Fix exiting select mode: Esc / inspector toggle-off now fully clear the
  session's DOM-selector chips (source of truth) so the overlays AND the inline
  prompt are dismissed together, instead of leaving them stranded.

Full-page raw-app editor:
- Add inspectorHoverOnly: the inspector highlights on hover (outline +
  name/size) but a click selects nothing — no persistent selection, no app-mode
  context pick, no inline prompt.

Context badges:
- ContextElementBadge gains a compact prop (passed only by AIChatMessage): the
  DOM-selector chip and tagged workspace items (app/script/flow) render smaller
  above a sent message, while every badge stays the same size in the composer.

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

* fix(sessions): address Codex review P1s on raw-app DOM context

- Inline element prompt: queue the turn when the chat manager is already
  streaming (mirror the composer) instead of a concurrent sendRequest that
  would race the shared abortController / streaming buffers.
- DOM selector chips now carry the raw-app path they were picked from. Each
  preview tab renders/pushes only its own app's chips, and a tab clears
  cross-app chips when it becomes the active DOM target — a selector could
  otherwise silently resolve against whichever raw-app tab is active
  (search_dom/read_dom target the active preview).

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

* chore(raw-apps): re-pin ui_builder artifact to f79e1c3

Bumps the UI Builder artifact to the release built from ui_builder main
after windmill-code-ui-builder#18 merged (the multi-select DOM inspector
synced with the host chat). This is the artifact re-pin step that gates
merging this PR.

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

* fix(sessions): inline-prompt vertical centering + auto-collapse DOM tools

- InlineElementPrompt: bias the textarea padding (pt-[5px] pb-[3px], same
  total) so the single line sits centered in the pill — the shared autosize
  floor otherwise adds slack at the bottom and the text read 1px high.
- search_dom / read_dom: drop the explicit `autoCollapseDetails: false` so the
  tool card collapses after a successful read like other tools (it was copied
  from get_app_runtime_logs, where staying open is intentional).

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

* fix(sessions): address Codex review on raw-app DOM access

- [P1] Release the DOM-requester slot when the active raw-app tab is hidden.
  Preview tabs stay mounted, so switching to a flow/script/page (or another
  raw-app) tab left search_dom/read_dom targeting the now-hidden raw app. The
  slot is now claimed with a per-tab owner token and released via the effect
  cleanup; the runtime only lets the claiming tab clear it, so a set/release
  race between two raw-app tabs can't blank the new owner.
- [P2] Strip the inspector outline classes from the clone ROOT (the selected
  element), not just its descendants — querySelectorAll skips the root, so a
  scoped read of the selected element leaked inspector-picked in its outerHTML.
- [P2] Reword the AIChatDisplay context-chip comment to drop drafting-history
  narration ("now", "not as rows here") per AGENTS.md.

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

* fix(sessions): scope a queued inline prompt to its own element (Codex P1)

An inline element prompt submitted while a turn was streaming got queued as
plain text; if the user selected another element before the queue flushed, the
queued turn snapshotted the new selection and sent scoped to the wrong element
(or none).

The queue now carries a context snapshot:
- queueMessage(text, context?) pins the selection present at submit time.
- sendRequest accepts contextOverride and uses it verbatim as the turn's
  selected context (feeds both the optimistic bubble and the API message).
- All three queue-flush sites thread the snapshot through and restore it if the
  auto-send bails.
- With an override, only the queued message's own DOM chips are consumed from
  the live selection — a newer selection made since is left intact — preserving
  the one-shot chip semantics.

onInlinePrompt snapshots getSelectedContext() when it queues.

(Codex's other P1 — full-page-editor hover-only no longer attaching elements to
App AI — is intentional per an explicit product decision, left unchanged.)

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

* fix(raw-apps): restore full-page editor App-AI inspector context

Codex/Pi flagged a real regression: passing inspectorHoverOnly disabled the
full-page editor's existing App-AI (app-mode) inspector context. Clicking an
inspected element no longer set inspectorElement, so the app-mode
SelectedContext badge and the element attach in prepareAppUserMessage were
dead.

inspectorHoverOnly and the session path (onInspectorSelect) are mutually
exclusive — the flag was only ever set in the full-page/app-mode context, so it
only suppressed the app-mode path. Remove it entirely (prop, guard, and the
pass in +page.svelte), restoring the original click -> inspectorElement ->
app-mode context behavior. Session behavior is unchanged; the hover highlight
still works in both.

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

* fix(sessions): snapshot inline-prompt context synchronously (Codex P1)

The immediate inline-prompt path called sendRequest without a context override,
so the selected element was read only after the async send preflight
(attached-file refresh, beforeSend, global-skill refresh). Picking another
element during that window attached the wrong one to the outgoing turn.

onInlinePrompt now snapshots the selection synchronously at submit time and
passes it as contextOverride on both the queued and immediate paths, so the
prompt always rides with the element it was scoped to.

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

* fix(sessions): drop a queued inline prompt's DOM chips when the app changes

A queued inline prompt scoped to raw app A described A's elements, but the
runtime's single DOM requester targets whichever preview is active. Switching
to app B before the queue flushed left the turn describing A while search_dom /
read_dom would query B.

When a raw-app preview tab becomes active it now also strips the queued turn's
DOM chips belonging to other apps (dropQueuedDomContextForOtherApps), so a
queued prompt can't ask the model to read one app's selectors against another's
live DOM. Non-DOM queued context is preserved.

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

* fix(sessions): scope inline prompt to its anchored element (Codex P1)

The inline prompt sits over a single element but snapshotted EVERY selected DOM
chip. With several elements selected (Shift-multi-select), a prompt shown over
B sent both A and B as context, so "change this button" couldn't identify which
one it meant.

onInlinePrompt now uses its anchored `selector` argument (previously ignored)
to keep only that element's DOM chip in the snapshot; non-DOM context is
preserved.

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

* fix(sessions): preserve original context on edit/retry (Codex P1)

restartGeneration resent with the live selection, so editing or retrying an
element-scoped prompt lost its DOM selector (or adopted a newer one) — DOM
chips are one-shot and cleared from the live selection after the first send.

It now passes userMessage.contextElements as contextOverride, re-using the
exact context the message was originally sent with. undefined (modes that don't
attach contextElements) falls back to the live selection as before.

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

* feat(sessions): route search_dom/read_dom to the selector's own app preview

The DOM requester was a single active-tab slot, so search_dom/read_dom always
read whichever preview was visible. A turn scoped to app A that read the DOM
after the user switched to app B would silently read B (Codex P1).

Preview tabs stay MOUNTED when hidden, so app A's DOM is still there — the fix
is to route by app instead of "active tab":

- Every mounted RawAppEditorView registers its DOM requester keyed by its app
  path (runtime holds a map, not one slot); the visible tab is tracked
  separately as the default target.
- search_dom / read_dom gain an `app_path` (surfaced per chip in the SELECTED
  DOM ELEMENTS block and the tool schema); the runtime routes the query to that
  app's still-mounted preview. No app_path → the active preview, or the only one
  open.
- If the named app's preview has been closed, the tool returns "The preview for
  X is no longer open…" so the chat can explain it, rather than reading the
  wrong app.

This makes the queued-context drop-on-switch (dropQueuedDomContextForOtherApps)
redundant — removed. The lightweight submit-time snapshot (which element the
message is about) is kept.

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

* fix(sessions): address Codex review on DOM access (script strip + app-scoped chips)

- search_dom/read_dom: strip the app's compiled <script> bundle from the
  whole-body clone so queries only see rendered HTML, not source (P1).
- DOM chip add-dedup and removal now match both selector AND app path, so an
  identical selector in another app can't drop or block a chip (P2).
- Refresh stale single-slot requester comments to describe per-app routing;
  reword the foreign-chip reset to reflect why it stays (composer chips are
  unlabeled by app) (P2).

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

* fix(sessions): scope DOM-chip clear to its app so a rebuild can't wipe another (Codex P2)

Every mounted preview emits inspectorClear on rebuild, routed to
onInspectorClearAll → clearSelectedDomElements(). Unscoped, a hidden app A
rebuilding cleared app B's active chip and highlight. clearSelectedDomElements
now takes an optional appPath; the preview-clear path passes its own path, while
post-send and foreign-reset clears stay unscoped.

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

* fix(sessions): reject a <script>-root DOM query so the app bundle can't leak (Codex P2)

The descendant strip (querySelectorAll('script')) skips the clone root, so a
search_dom / read_dom query whose selector targets `script` serialized the whole
compiled bundle. Reject a script root with an explanatory result before cloning.

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

* fix(sessions): union queued inline-prompt DOM context + reject inspector-label root (Codex)

- Multiple inline prompts queued during one stream accumulate their text, but
  queuedContext replaced (last wins) — dropping an earlier element's chip and
  misapplying its instruction. Union the DOM selector chips across queued prompts
  (non-DOM context still from the latest snapshot) (P1).
- search_dom/read_dom: reject an .inspector-label clone root, like the <script>
  root — the descendant strip skips the root, so a `.inspector-label` query
  would return inspector chrome (P2).

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

* fix(sessions): restore consumed DOM chips when a turn is cancelled unsent (Codex P1)

DOM selector chips are consumed from the live selection before the request goes
out. If the user hits Stop before any usable output, restoreUnsentTurn put the
text/pastes/images back but not the chips, so resending the restored prompt lost
its element scope. Re-add the consumed chips on rollback (skipped on a queued-
message handoff, which carries its own context).

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

* fix(sessions): edit box edits the message's own context, sends what it shows (Codex P1)

Editing a past message bound the edit box to the LIVE selection while the resend
carried the message's original contextElements — so the box showed one set of
chips (or none) but sent another, and add/remove in the box did nothing. The edit
box now edits a copy seeded from the message's own contextElements, and
restartGeneration sends that edited copy; a bare retry still falls back to the
original. Drops the now-unused selectedContext prop from AIChatMessage.

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

* fix(sessions): keep DOM context coherent on dequeue/cancel + per-chip edit identity (Codex)

- dequeueMessage dropped the queued draft's pinned DOM context, so returning it
  to the composer sent the live selection instead. Restore the draft's context on
  dequeue (P1).
- Cancel-rollback re-added this turn's chips additively, mixing in chips selected
  mid-stream. Replace instead so the restored draft stays coherent — shared helper
  #restoreDomContext used by both paths (P1).
- The context chip row keyed/removed by (type, title); repeated DOM elements share
  a title (two button.btn), so editing a multi-select message gave duplicate keys
  and deleting one chip removed both. Identify DOM chips by (appPath, selector) (P1).

The queued-prompt instruction↔element association (Codex #1) is the accepted union
tradeoff and is unchanged.

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

* fix(sessions): scope read_dom pagination + don't consume live chips on edit/retry

Codex P1s:
- read_dom's continuation note only renamed read_file, so following it with
  start_line alone dropped app_path/selector and re-read the active app's whole
  body instead of continuing inside the element.
- contextOverride also carries an edit/retry's replayed copy of an older
  message's context. Consuming it from the live ContextManager stripped an
  identical chip the user had since selected in the composer, leaving their
  draft unscoped. Overrides now declare their origin ('pinned' vs 'replay').

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

* fix(sessions): keep DOM context paired with the draft it belongs to

Restoring a draft's pinned DOM chips replaced the live selection unconditionally,
but both restore paths can leave a different draft in the composer:

- Cancel: an occupied composer declines the rolled-back text (restoreInstructions
  bails), yet the cancelled turn's chips still replaced the live ones — the draft
  the user typed during the stream kept its text but got retargeted.
- Dequeue: queued text is prepended onto an existing draft, so both instructions
  share one composer; replacing the chips dropped the standing draft's element.

restoreInstructions/prependText now report whether the composer took the text.
Cancel restores context only when it did; dequeue keeps both drafts' chips when
it merged into one.

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

* fix(sessions): scope an inline prompt's chip filter to its own app

The filter narrowing a multi-select down to the anchored element matched on
selector alone. Selectors are generated per app and collide across them, so an
inline prompt in app B also carried app A's identically-named chip, leaving the
model two indistinguishable referents. Since draft restoration can now
legitimately hold chips from several apps, match (selector, appPath) — as the
sibling deselect/clear handlers in this file already do.

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

---------

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