Commit Graph
148 Commits
Author SHA1 Message Date
Guilhem LemouelandClaude Opus 5 559ffb7285 feat: one breadcrumb in the page header, and the banners that used to sit under it
The header band now carries a single breadcrumb — workspace, fork, then the item's
path — built from one component, so every segment has the same size, weight and icon
size, and slashes separate them. The item's kind icon rides with its summary, after a
dot, rather than with the path.

The fork and drafts banners move into the band as badges plus a subtle Review button:
ahead/behind counts instead of a bare change total, and the per-kind breakdown stays on
the compare page. The home hero's CLI, MCP and Hub links move into an ellipsis menu,
alongside the toggle that pins the session chat to the page.

The flow editor and the app viewer hand their bar to the band too, and a route can drop
the band's bottom border when its own content draws one — the border stays in the box
with a transparent colour, so contents never shift a pixel across a navigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:22:24 +02:00
Guilhem Lemouel 9a0aebbe3b Merge remote-tracking branch 'origin/main' into enable-operator-ai-chat-nav
# Conflicts:
#	frontend/src/lib/components/flows/FlowEditor.svelte
#	frontend/src/lib/components/sessions/OpenInSessionButton.svelte
#	frontend/src/routes/(root)/(logged)/+layout.svelte
2026-09-21 11:13:27 +02:00
Guilhem LemouelandClaude Opus 5 e0a33dc3d7 wip: one sidebar for operators and developers, with a detach mode
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 11:09:56 +02:00
6186a0645d feat: edit variables, resources and triggers in their own session tab (#11206)
* feat: edit variables, resources and triggers in their own session tab

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

* fix: take a trigger's flow kind from its config and name the written trigger

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

* refactor: one operating-workspace context for editors acting on a session's workspace

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

* fix: reload page item tabs only in sessions acting on the tool's workspace

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

* fix: resolve open in workspace for every preview tab kind in one place

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

* fix: guard every component under a session editor against the navigation workspace

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

* fix: show a page item tab whose item is gone instead of mounting its editor

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

* fix: follow a saved page item in the tab's own workspace and restore in-frame rows

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

* feat: close a page item tab from its editor header

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

* fix: judge trigger permissions by the user acting in the session workspace

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

* fix: judge every session-editor permission by the user acting in that workspace

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

* fix: keep session permissions reactive to the acting role and restore tabs as last seen

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

* fix: default Path's owner controls to the user acting in its workspace

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-19 23:29:14 +02:00
f00b2fcb1e feat: drafts follow their item through a move; behind means base ≠ head (#10577)
* refactor: give home multi-select a reserved gutter and a menu entry

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

* fix: keep checkbox theming and reserve the gutter on non-selectable rows

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

* feat: carry every draft with an item when it moves

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

* feat: move draft-only items and warn editors when an item moves

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

* refactor: put the home selection checkbox back in the kind icon slot

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FaEacdxR6M6VDej6C9r39

* feat: animate the home bulk bar and exit selection at zero

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FaEacdxR6M6VDej6C9r39

* fix: keep dialog icon badges round and the panel inside narrow viewports

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FaEacdxR6M6VDej6C9r39

* fix: address review findings on the draft-carry path

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FaEacdxR6M6VDej6C9r39

* fix: keep a staged rename when a move carries the draft

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

* fix: restamp only the deployer's own carried draft

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

* fix: scope the moved-save restamp to the mover as well

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

* fix: read the app move's author from the head version, not the draft's base

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

* fix: carry a flow draft's baseline path so deploying it cannot un-move the flow

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

* fix: reject unsupported kinds in move_draft, survive NUL-poisoned draft rows

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

* fix: skip NUL-poisoned rows in every draft-value rewrite, not just the first

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

* fix: report a NUL-poisoned draft on move instead of 500ing

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

* fix: name the attempted operation in the NUL rejection message

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

* refactor: drop dead selection code and comments that outlived their state

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

* docs: describe script staleness as head-pinned, which is what the loader does

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

* docs: correct the third staleness comment left claiming a stable fork base

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

* fix: address CI review — auth order, save race, carry failure, path validation

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

* fix: gate operators earlier, skip the write tx without lineage, unblock a chained move

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

* fix: run the post-write moved re-assert under RLS, not the raw pool

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

* test: pin the moved answer to what the saver can see

The post-write re-assert names a path and a username, and nothing at any
layer stopped it reading them off a raw pool connection. Swapping the
transaction back to `db.begin()` compiles and passes everything else, so
the guard has to be a test: a non-admin saving at a path whose item moved
into a folder they cannot see gets `saved`, while the admin gets `moved`.

Also drops two doc comments still arguing that clearing the write gate at
the old path removes the need for an RLS envelope. It does not — the gate
resolves the old path and the re-assert asks about the new one.

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

* docs: name the real deploy path and stop restating the RLS constraint

`update_path` is not a symbol in this repo; a script move goes through
`create_script`. The re-assert's comment re-derived the disclosure argument
that already sits on `resolve_moved_to_in`, where a caller would break it.

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

* docs: state the RLS and restamp constraints once each

The RLS envelope was argued at three sites in drafts.rs; it now sits only on
`resolve_moved_to_in`, whose signature is what a caller would break. The
restamp scoping was copy-pasted at all three deploy call sites while already
documented in full on `move_drafts_for_path`.

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

* fix: carry both path keys on a move, and grant the draft sequence

The upsert now runs as `windmill_user`, so it calls nextval on `draft_id_seq`
as that role. The only thing granting that is the ALTER DEFAULT PRIVILEGES in
20250205131523, whose DO block swallows failures — so an instance where it
errored would fail every autosave with `permission denied for sequence`.

A draft value carries two path keys: the typed one and a mirror the editors
keep in step with it while it differs from the row's path. Rewriting only the
typed one left the mirror naming the old location, and the loaders prefer the
mirror — reopening a moved session script restored the old path and the next
save un-did the move. Both keys now follow, in the move endpoint and in the
passive carry, under the same tri-state rule.

`typed_path_field` answered `draft_path` for every non-script kind, including
resources, variables and triggers, which have no such key. It returns `None`
for them now, and `move_draft` reads its guard off that mapping so the movable
set and the field mapping cannot drift apart.

Also documents that `move_drafts_for_path` mutates every owner's row and
enforces nothing itself, and parses the draft payload once per save instead of
three times.

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

* test: pin the two-key move, and stop the down migration breaking instances

Revoking the sequence grant would strip a privilege a healthy instance had
before this migration ran — the grant it adds is indistinguishable in the
catalog from the one ALTER DEFAULT PRIVILEGES gives at creation time — so the
down is a comment, matching the other grant-only migrations.

The mirror rewrite is spread over three sites that have to agree and fails
silently when they don't, so it gets a test: a draft carrying both path keys
has both moved, and one carrying neither mirror does not gain one. It reads
the value back over HTTP rather than with `sqlx::query!`, which would need an
offline cache entry of its own.

Also drops twelve `.sqlx` entries this branch added and then superseded, and
corrects the doc and openapi text that still described only the typed path
being rewritten.

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

* docs: point the empty down at the grant it is declining to revoke

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

* feat: drop the restamp and tri-state; a move relocates the draft row only

A deploy that renames an item is a deploy like any other: every draft on the item
goes stale, and the stale prompt with its diff is the single mechanism to catch up.
move_drafts_for_path now touches only the row's path column, so the value keeps the
base version the draft actually forked from, and the "moved" patch carries no
version restamp. DraftBaseVersion shrinks to the three per-kind lineage fields.

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

* feat: stale prompt links to a diff that names and lets you pick the deployed version

The stale-draft prompt gains "See what changed", which opens the diff drawer. The
drawer resolves the deployed side by the draft row's own path (not the typed path,
which after a rename still names the archived row), labels which version the left
pane is, and offers a picker over the item's deployed history for scripts, flows
and raw apps. The history endpoints return created_by (and created_at for apps)
so each entry can name its deployer. "Restore to deployed" moves to the header
actions.

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

* test: move_to asserts the response status

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

* fix: keep a script draft's base at the version it forked from

The script editor seeded the draft's parent_hash from the deployed head on every
load, and the next autosave persisted it, so a draft behind the deploy read as up
to date after being opened once. The base now comes from the draft when one
exists; the head is only used for a fresh checkout or an explicit topHash. Deploy
already fetches the live head and confirms on mismatch, so the base is what makes
that check meaningful. The webhook "run this version" URL uses the deployed hash
rather than the draft's base.

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

* feat: store the version a draft forked from in one draft.base column

Every kind kept its fork base under a different name and type inside the
value: parent_hash (hex) for scripts, version_id for flows, parent_version for
apps. draft.base holds it as one text id, derived on save from the value so
every writer fills it the same way, backfilled by the migration (rows holding a
NUL are skipped, since ->> raises on them). The get-by-path overlay exposes it
as draft_base and the drafts list as base; the editors and the compare page
read that one field and compare it to the head as text.

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

* feat: raw-app drafts carry a fork base, so behind means base != head for them too

The raw-app bundle never carried the version it forked from, which left raw apps
on the timestamp check that self-heals as you type, and the header's deploy guard
read a version prop nothing set, so deploying over a newer version never asked.
The route now stamps parent_version into the bundle (the draft's own base when it
has one, else the head), the server derives draft.base from it, the stale prompt
compares it to the head and links to the diff, and the editor threads it to the
header so the deploy guard confirms. A deploy re-pins the base to the version it
wrote.

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

* feat: refuse a rename onto a path that already holds a draft

A draft occupies its path the way a deployed item does: a never-deployed item,
or a draft left on an archived script. Renaming onto it would either merge two
items or leave the losing row stranded at a path its item has left. The move now
refuses with a BadRequest inside the deploy's transaction, so the rename itself
fails and the source stays deployed. Every draft on the item then moves; there is
no longer a left-behind count to report.

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

* feat: save drafts by row id, so an open editor follows its draft through a move

A rename carries every draft on the item to the new path. An editor left open
across it was still saving by the path it opened on, which the server had to
refuse and answer with where the item went (the "moved" handshake and its
modal). The draft row has an id: the get-by-path overlay now returns it as
draft_id, every later save sends it, and the server writes the row wherever it
is and answers with that path. The editor then follows: it flushes what it holds,
tells the user, and navigates to the item's new path, where the stale prompt
says what changed. The lineage-based move resolvers, the moved status and the
moved modal are gone.

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

* feat: the out-of-date prompt names both versions and can take the latest as the new base

The prompt now says which version the draft forked from and which is deployed
(and by whom), instead of two timestamps, and gains "Take latest, keep my
edits": the draft's base moves to the head and its content stays, so the user
can acknowledge a newer version without discarding their work. Each route sets
its kind's base field on the draft value and persists it; the raw-app bundle
carries it already, so setting the state is enough there.

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

* feat: two-action out-of-date prompt; taking the latest moves into the diff drawer

Four buttons made the prompt hard to read. It keeps "See what changed" and a
red "Use latest" (it replaces the draft); closing it is keeping the draft.
"Take latest, keep my edits" moves to the diff drawer's header, offered only
while the draft is behind, so the user takes the latest with the diff in front
of them. Scripts, flows and raw apps pass the action through their diff drawer;
the classic app editor has no drawer wired to the prompt and loses it.

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

* chore: drop the draft_id_seq grant; the draft upsert runs on the raw pool

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

* fix: a moved draft's path keys follow it, and a refused rename names the draft's owner

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

* fix: follow a moved draft on tab close, and deploy a followed flow at its new path

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

* fix: write a followed draft by id against the row's own path keys; keep base on assign and clone

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

* fix: look up a script's head at its row path, and show flow and app version ids bare

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

* fix: session editors save by draft id; raw apps keep a legacy draft's base unknown

Also advance the raw-app base on deploy, relocate once per move, drop the
hoisted operator check and the unread base on drafts/list.

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

* fix: guard a base-unknown raw-app deploy against the head at load; keep the base in session hydration

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

* refactor: the server follows a moved draft through a move record, not client-sent row ids

A move writes old path -> new path (per workspace and kind, per owner for a
draft-only move) in its transaction; a draft save or discard addressed to a
path the caller has no draft at resolves through it and keeps the moved
draft's path keys. Creating an item at a path drops the records leaving it.
Every writer (edit routes, sessions, chat, CLI, the tab-close flush) follows
without passing an id, so the id plumbing is gone.

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

* fix: session loaders keep a draft's base, and a failed relocation flush stays put

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

* fix: a draft-only app move refuses the other app kind; a session keeps an unknown base unknown

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

* test: pin a teammate's carried draft; name the kind that refuses a draft move

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

* fix: an unknown base stays unknown in every loader, and an owner move extends an item move

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

* fix: a clone keeps only a base it can resolve; a base-unknown script deploys without a false guard

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

* fix: a workspace clone sanitizes a NUL-bearing draft instead of copying it unstripped

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

* fix: move records follow an account rename and deletion; a legacy draft says why it cannot move

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

* fix: an owner move extends only the item's own route, not another user's

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

* fix: a redeploy ends a route off its path, take-latest persists on raw apps, stale picker loads are dropped

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

* fix: a poisoned draft's path keys follow a move, legacy only bypasses routing on a delete, picker loads are generation-guarded

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

* fix: count picker load generations, and report a skipped legacy upsert as a conflict

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

* fix: a legacy discard follows the item's move record too

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

* fix: a failed version load keeps the picker on what the diff shows; one spelling for a legacy delete

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

* fix: the picker marks the version on display as head, restore compares the head, relocation follows the last move

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

* fix: say so when a version fails to load in the diff picker

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

* fix: take latest re-reads the head at click time; type the kept head as prepared diff data

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

* fix: taking the latest moves the head each editor knows, not just the base

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

* feat: take latest adopts the head the diff shows, and is offered while the drawer sees the draft behind

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

* fix: a head nobody could name is not behind, so take latest is not offered without one

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

* fix: the flow drawer's head is the version its payload came from, and its callback type says so

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

* fix: a NUL in a move's summary is dropped, and take latest simply adopts the head it was handed

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

* fix: a loaded raw-app draft keeps its own fork base, and an unknown head is refused

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

* fix: a routed discard names where it landed, a superseded drawer opening is dropped, and a loaded draft keeps its base in every editor

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

* fix: a legacy draft occupies its destination, a superseded opening writes nothing, and a loaded flow draft keeps no base it lacks

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

* fix: the drawer owns its opening, a loaded script draft keeps no base it lacks, and a legacy occupant says who can clear it

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

* fix: taking the diff drawer without a token claims it, and the classic app editor takes one

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

* fix: a retried routed discard still names the destination, and filling the drawer takes the opening too

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

* fix: a no-op routed discard names the destination only to someone who could write there

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

* fix: the no-op routed discard gates its answer on reading the destination, and a session draft keeps its unknown base

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

* fix: abandoning an opening clears the drawer it still owns

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

* fix: an app deploy pins only a version it wrote as the next draft's base

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

* fix: the deploy-override diff takes an opening its editor can hand back

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

* refactor: pin the version this deploy wrote even when one landed on top, and tighten three comment blocks

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

* fix: a deploy claims only the version it appended to the head it read, and names the head separately

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

* fix: a deploy always names the head it left behind, and pins a base only when it can claim one

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

* fix: the derived base is read after the sanitizer, and a deploy that claims nothing leaves no base to compare

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

* fix: the route's lineage follows an in-place deploy, and the raw-app editor's event type carries the head

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

* docs: the raw-app deploy comment says what that editor actually does with version

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

* fix: a group member can be told where their item went, and a deploy names the head's author

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

* fix: an emptied selection is no shift anchor, and a deploy leaves no draft for the prompt to compare

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

* fix: session tabs compare the same base pair, and a consumed draft is not out of date

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

* fix: a failed anchor read is not a raced deploy, and take latest closes only its own drawer

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

* fix: an unclaimed deploy always confirms, and the prompt keeps warning a loaded teammate draft

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

* docs: the base-unknown confirmation says what it knows, and two comments match the guard

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

* fix: the other app kind collides whoever owns it, and session tabs get a head to compare

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

* fix: the cross-kind refusal reads properly, and a session flow keeps its own response's head

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

* fix: a fresh session checkout takes the head its payload came from, and a deploy keeps the base it pinned

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

* fix: the move endpoint validates its source path, and two comments say what their branch does

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

* fix: an unanswered head read confirms rather than assuming the app editor is current

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

* fix: a deploy is not blocked by the draft a move carried to its destination

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

* fix: an unread head confirms with the copy for caution, not for an observed deploy

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

* fix: the move record alone excuses a carried draft at the destination, whoever owns it

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

* feat: an app deploy answers with the version it wrote, so the editor stops inferring it

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

* test: the rename assertion reads the deploy's json answer

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

* fix: the unread-head warning reads as caution in the deploy drawer too, and the cross-kind refusal names a remedy

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

* fix: a reused destination retires the routes pointing at it, and draft_base stays out of diffs

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

* fix: the app head is the tail of app.versions, not the newest timestamp

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

* fix: app history lists in deployed order, so the picker numbers it right

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

* fix: the ordering test's setup sql compiles offline, and the head join names its app

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

* fix: kinds that cannot move skip the move lookup, and the move wording needs read

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

* perf: a deploy history comes a page at a time, so the diff drawer opens at once

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

* fix: a history stays whole unless asked to page, and pages inside the version array

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

* fix: an asked-for history page is bounded, and a failed one is not the end of the list

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

* fix: an unasked history is whole again, and an absurd page is empty not an error

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

* fix: naming only a page still asks for one, and a stray version stays reachable

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

* fix: a fork's nul-poisoned draft arrives clean, so its dangling identity repoints too

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

* fix: a raw app names its deployed version even when the history will not load

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-18 20:54:23 +02:00
GuilhemandClaude Opus 5 3b4e13d1c5 fix: key the large root font size on screen width, not window width (#11216)
* fix: key the large root font size on screen width, not window width

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

* fix: move the flow preview button offset to the screen-width query

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 14:51:32 +02:00
Ruben FiszelandClaude Fable 5.1 e8c02c04cd feat: windmill-chat sdk for chat-mode flows in external frontends and raw apps (#11117)
* feat: windmill-chat sdk for chat-mode flows in external frontends and raw apps

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

* fix: keep streamed answers until persisted, finish turns after history fallback

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

* feat: ai sdk transport and assistant-ui runtime for windmill-chat

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

* fix: finish a turn from the flow result until its answer row lands, hash chat ids without crypto.subtle

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

* fix: judge a turn answered by a persisted assistant row, wherever it was fetched

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

* fix: attribute a turn's answer to its own jobs, keep a local turn when switching conversations

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

* fix: mirror local history on every change, attribute failure-handler answers to the turn

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

* fix: new chat per token string in the React hook, idle after destroy, no reorder on view

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

* fix: recreate the hook's chat on any credential change, namespace local history per user

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

* fix: send the latest inputs from the React hook

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 22:32:45 +02:00
Ruben FiszelandClaude Opus 5 e651b4cd63 perf: lazy-load the low-code runtime on public app pages (#11087)
* perf: lazy-load the low-code runtime on public app pages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018bSzETfUb23CRRMEJdwqeS

* perf: fetch the low-code runtime alongside the app payload

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018bSzETfUb23CRRMEJdwqeS

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 11:14:09 +00:00
Ruben FiszelandClaude Fable 5.1 fdd3b36423 feat: workspace setting to hide the AI assistant, agent steps unaffected (#10941)
* feat: workspace setting to hide the AI assistant, agent steps unaffected

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

* fix: load workspace AI config on cold /sessions load and say hidden, not disabled

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

* fix: follow workspace switches on /sessions gate and drop deprecated button size

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

* fix: key the /sessions hidden-assistant gate on the acting workspace's own config

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

* fix: tag the /sessions hidden-assistant verdict with its workspace and drop superseded reads

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

* fix: overlay the /sessions hidden-assistant gate so warm sessions survive workspace switches

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

* fix: hide the pipeline insert menu AI prompt and refuse chat turns where the assistant is hidden

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

* fix: shrink the home Build with AI / CLI / Hub line to a flush hint row

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

* fix: frame the workspace toggle as hide AI sessions at the bottom of the AI settings

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 17:21:10 +02:00
GuilhemandClaude Opus 5 b57e231c2b fix: keep raw-app editor selection consistent across sidebar and tabs (#10885)
* fix: route raw-app editor selection through one switch function

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* fix: stop announcing folders as selected from the file tree

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* fix: carry the selection through a folder rename

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* fix: keep the generated wmill.ts tab out of stale-tab cleanup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* refactor: test document existence through one predicate

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* refactor: route the history replay through the same predicate

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* fix: clear the selection in the same tick a runnable is deleted

Deleting the selected runnable dropped it from `runnables` and left the
editor to notice via the stale-tab effect, one frame later. In that window
the pane rendered "No runnable at id <key>".

The sidebar list now reports the delete instead of mutating `runnables`
itself; the editor deletes and closes the tab together, so the selection
moves through `select` synchronously. The stale-tab effect stays as the
backstop for deletes that come from elsewhere.

Also retitle the two sidebar create buttons and rename the FileExplorer
exports behind them: both have always anchored on the selected file's
parent folder, never the root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

* refactor: require the runnable delete callback

Optional, the row's Delete button renders and does nothing. There is one
caller and it always supplies it, so the compiler can hold that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LPVrfeXbjznotqG7JdF4H

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 14:13:32 +00:00
Diego ImbertandClaude Fable 5 7751d3e43e feat(frontend): warn when COEP blocks cross-origin resources in raw app editor preview (#10328)
* feat(frontend): warn when COEP blocks cross-origin resources in raw app editor preview

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

* fix(frontend): hedge COEP toast wording and attach warning on detached preview initial load

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

* chore(frontend): condense COEP warning rationale comment

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 08:50:23 +02:00
574775d50c fix: teach the AI the raw-app job bindings, the SDK reference and the draft/deployed split (#10754)
* feat: teach the AI the raw-app job bindings and the draft/deployed split

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

* fix: scope the raw-app deploy advice to the referenced item, and stop kind-conversion from stranding fields

The draft/deployed guidance added in the previous commit was read as "deploy the
app too": the agent asked for both the flow and the app and routed a one-item
dependency through the review-and-deploy page. Only the referenced flow or
script has to exist deployed — the preview runs the app's draft — so the prompts,
the `write_app_runnable` warning and the testing rule now say to offer that one
deploy and leave the app a draft.

`buildPersistedRunnable` spread the existing runnable when rewriting it, so
converting a path runnable to inline left `runType`/`path` behind (and the
reverse left `inlineScript`). `isRunnableByName` matches the inline branch
first, so an app "wired to a flow" silently ran stale inline code.

`test_run_app_runnable` now fills ctx-bound inputs with `$ctx:<prop>` the way
RawAppBackgroundRunner does, so a ctx argument no longer arrives missing.

The SDK-reference rationale claimed WM_TOKEN may be unset, that a missing base
URL falls back to localhost, and that a job token is scoped enough to 403 a
hand-rolled REST call. None of the three is true, and it shipped to every
write-script prompt; the text now only says the client configures itself.

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

* fix: address review round on the raw-app AI instructions

The eval case could pass on the exact answer it exists to reject. Every
`requiredMentionsAnyOf` alternative but one was flow-agnostic, so "the app must
be deployed" satisfied "must be deployed". All alternatives now name the flow,
and a unit test pins that the app-only phrasing fails.

`instanceLine` asserted "self-hosted Community Edition" outside the browser,
where `isCloudHosted()` reads false and the license store is unset — so every
global eval was told that regardless of what it pointed at. It is now emitted
only under BROWSER.

`assistantExpect.forbiddenMentions` defaulted a missing `assistantText` to "",
which passes every entry forever on a mode whose runner does not report it.
It now fails with that as the reason.

`buildPersistedRunnable` carried `schema` across a retarget, so a path runnable
pointed at a new flow kept the previous item's schema and `genWmillTs` typed
`backend.<key>(args)` from the wrong inputs. It survives only while kind and
path both match.

The SDK header claimed "a function that is not listed below does not exist".
`windmill-client` also exports the generated services, and the Python client
exposes `Windmill.get`/`.post`, so an endpoint without a helper had no legal
move. Each language now names its own escape hatch.

`getAppInstructions` said the attached reference carries the TypeScript SDK even
when `language: "python3"` had swapped in the Python one — on the very sentence
telling the model to make that call.

The kind-conversion comment claimed a hybrid runnable "silently runs stale
inline code". It does not: `isRunnableByName`, `isRunnableByPath`,
`convertPersistedToBackendRunnable` and `rawAppPolicy.processRunnable` all
dispatch on `type` alone. The leftovers contradict the runnable's kind rather
than override it, which is what the comment now says.

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

* fix: address round-2 review nits on the raw-app AI instructions

`flow is deployed` was satisfied both by "once the flow is deployed, the button
works" and by a hallucinated "done — the flow is deployed", which eval mode makes
impossible and the drafts-only judge cannot see. Every alternative now states an
outstanding obligation, and two more real phrasings ("will need to be deployed")
are accepted so a correct answer is not failed on wording.

Condenses the three comment blocks that ran past the four-line limit in
AGENTS.md, and drops two claims inside them that no longer hold: the
`testRunAppRunnable` doc said it runs a runnable the way the app's own frontend
does (it is the editor preview, which a deployed app's stored policy does not
match), and `undeployedRunnableTargets` described its argument as the write
tool's raw input when the call site passes the persisted runnable.

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

* fix: report the real cause when a test run fails, and label the app-runnable card

Driving `test_run_app_runnable` in a live session surfaced two defects the
API-level check could not see.

`executeTestRun` built its failure message from `error.message`, which the
generated client leaves as the bare status text while the server's message sits
in `body`. A path runnable aimed at an undeployed flow reported "Not Found"
instead of "Not found: flow not found at name u/admin/current_time" — dropping
the one diagnostic the run exists to produce. `formatToolError`, in the same
file and written for exactly this, now does it. This also applies to
test_run_script and test_run_flow, which had the same loss.

The completion card read "Flow test completed successfully" for an app runnable,
because `contextName` doubles as the jobs-tray kind and a path runnable pointing
at a flow really does queue a flow job. A `completionName` override now names
what ran without changing the kind.

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

* test: pin the deploy expectation against wrong answers, not just correct ones

`deploying the flow` was satisfied by "done deploying the flow" — a deploy the
agent only claims to have made, which eval mode makes impossible and the
drafts-only judge cannot see. Replaced with the prospective forms, and dropped
the same reading from the workflow variant.

Three review rounds each found this same class of hole in the phrasing list, so
the list is now exercised against the wrong answers themselves rather than
eyeballed: naming the app as what needs deploying, claiming the deploy is
already done, claiming to have deployed the flow, and saying nothing about
deploying all have to fail, while four real correct phrasings have to pass. The
test reads the case out of global.yaml, so a future edit to the alternatives is
checked by it.

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

* test: drop the tense-neutral deploy alternatives and cover completed claims

A gerund after a preposition carries no tense, so `before`/`after`/`by deploying
the flow` all match a deploy the agent only claims to have made ("after
deploying the flow, I clicked the button and it returns the greeting") just as
the bare gerund did. All three are gone rather than swapped for whichever reads
least badly, and the two completed-deploy phrasings are now negative fixtures.
The remaining alternatives are imperative or obligational, which a claim of
having already deployed cannot satisfy.

Condenses the two comments this list carries: the YAML block to four lines, and
the test's rationale to the durable constraint about substring matching.

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

* fix: encrypt sensitive inputs when test-running an app runnable

`test_run_app_runnable` sent `force_viewer_static_fields` but not
`force_viewer_sensitive_inputs`, which every other preview path derives from
the runnable's `sensitive` user fields. That list is the only thing driving the
encryption loop in apps.rs, so testing a runnable with a sensitive input wrote
the real value into the job's args in plaintext, readable by anyone with run
access to the workspace.

Verified against a running EE instance. With the list, `api_key` is stored as
`$encrypted:mvqtSRI9…` and the sentinel appears nowhere in the job record;
without it, the sentinel is readable in run details. A non-sensitive field is
left plaintext either way.

The tool claims parity with the editor preview, so it uses that same filter
(`type == 'user' && sensitive`) and omits the field entirely when empty.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-08-20 11:17:33 +02:00
494e6f146e fix: route legacy AI entry points to sessions instead of the unmounted chat (#10705)
* fix: route legacy AI entry points to sessions instead of the unmounted chat

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

* fix: keep createSession's workspace choice and revert pipeline hand-off

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

* fix: guard in-session step generation and restore AI action labels

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

* fix: keep AI Fix usable in-session and stop silent no-op hand-offs

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

* fix: neutral AI form assistant heading to match both branches

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

* docs: state the AI form assistant branch rationale once

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

* feat: auto-send AI hand-offs and keep in-session step generation in global mode

* fix: name the AI session in the entry point labels

* fix: claim auto-send reactively and queue programmatic sends mid-turn

* test: pin the auto-send claim going stale

* fix: stop the script drawer hand-off from abandoning its unsaved script

* fix: keep a stale hand-off prompt and close the pre-loading send window

* fix: only blank the composer for an intent this wrapper can claim

* fix: report composer edits only, never the mount-time draft

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-08-18 17:06:51 +02:00
Guilhem d40a446868 style: use subtle Button for raw app preview toolbar actions (#10747)
* style: use subtle Button for raw app preview toolbar actions

* fix: expose inspector toggle pressed state via aria-pressed
2026-08-18 12:31:42 +02:00
343ce6e143 fix: derive a raw app's policy on deploy, and default an omitted execution_mode (#10733)
* fix: default an omitted app policy execution_mode to publisher

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

* docs: drop stale comments claiming execution_mode is required

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

* fix: derive a raw app's policy on deploy instead of trusting the caller's

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

* chore: pin the ee ref to the companion branch

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

* fix: vendor the raw-app policy derivation into the bundle job

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

* docs: note the vendored raw-app policy bundle

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

* fix: derive the policy on a value-only raw-source update too

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

* fix: reject raw-app runnables whose shape yields an unusable grant

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

* fix: cache the new policy query and tighten raw-app runnable validation

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

* fix: let the policy bundle drift guard survive a CRLF checkout

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

* chore: update ee-repo-ref to 23431f5cf1d627051ded89111bbf2e301e9db456

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

Previous ee-repo-ref: 0bdf8818fa115ad6b0d14f3117a18e8a580cce4d

New ee-repo-ref: 23431f5cf1d627051ded89111bbf2e301e9db456

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-08-18 01:52:10 +02:00
Ruben FiszelandClaude Opus 5 39f0542b2f refactor(frontend): keep the shared-utils bundle free of UI code (#10735)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 00:36:00 +02:00
Ruben Fiszel ceacc17014 fix(raw-apps): respect the instance .npmrc in the raw app editor (#10629)
* feat(raw-apps): route in-browser npm installs through the npm proxy

* fix(npm-proxy): follow npm range semantics and cache packuments

* fix(npm-proxy): bound the packument cache by bytes and stream tarballs

* fix(npm-proxy): keep a v-prefixed pin exact and read the tarball once

* chore(raw-apps): bump the ui_builder pin to the npm-proxy installer
2026-08-11 12:30:40 +02:00
Ruben FiszelandClaude Opus 5 8c65511e81 fix: raw app new-app modal ignores instance-level AI settings (#10619)
* fix: stop the new raw app modal claiming AI is unconfigured before it knows

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

* fix: require a resolved workspace before trusting the loaded AI config

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

* fix: drop the unreachable token guard and point superadmins at instance settings

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

* fix: show the instance settings link to superadmins who are not workspace admins

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:39:38 +02:00
GuilhemandClaude Opus 5 616d4fe167 fix: edit-in-dev-workspace dead-ends, wraps, and misses the tree view (#10354)
* fix: stop the homepage edit-in-fork button from wrapping

* fix: show the full edit-in-fork label anywhere on the button

* fix: thread showEditButton through the homepage tree view

* fix: match the edit-in-fork button styling to the normal edit button

* fix: edit in dev workspace dead-ends on items the dev workspace lacks

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

* fix: pull the item's folder before copying it into the dev workspace

* fix: speak the compare page's update vocabulary in the dev-workspace prompt

* fix: raw app with no stylesheet was undeployable across workspaces

* fix: drop the raw-app stylesheet workaround now that the backend serves one

The frontend wrapped `getRawAppData` to report a missing `.css` as empty,
because a raw app with no stylesheet stores no css blob and the shared deploy
treats the resulting 404 as fatal. #10364 fixed that at the source: the backend
now serves an empty body for a missing stylesheet, so the wrapper guards a 404
that no longer happens.

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

* fix: drop the fork icon from the edit-in-dev-workspace affordances

The row button carried both a pen and a fork, and the menu entries and detail
page buttons carried a fork alone — where the menus already used that same icon
for Duplicate/Fork, so the two entries were indistinguishable. The action is an
edit, so it takes the pen everywhere, matching the ordinary Edit button.

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

* fix: send edit in dev workspace to the item's editor

The affordance landed on the item's page in the dev workspace and left the user
to open the editor from there. It says "Edit", so it goes to the editor:
`/scripts/edit/...?workspace=<dev>` and the equivalent for flows and both app
kinds. `?workspace=` still does the workspace switch, which the logged layout
applies on any route.

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

* feat: choose the on-behalf-of user when updating the dev workspace

The prompt deployed the item with no say over the identity it would run under,
so an item that ran on behalf of someone in prod silently became the deploying
user's in the dev workspace. It now offers the same choice the compare page
does, under the same rules: shown only when the source item carries an
on_behalf_of, picking anyone but yourself gated on admin/wm_deployers in the
target, and confirming blocked until a choice is made — including while the
lookup that decides whether one is needed is still in flight.

The prompt also stops offering the compare page inline; the confirm button
still leads there when the user can't deploy into the dev workspace.

Two fixes the reused selector needed to work inside a dialog:
- ConfirmationModal takes `confirmDisabled`, which also blocks the Enter binding.
- The popover's z-index is now overridable, and the user picker is portalled.
  A ConfirmationModal renders above the popover layer, and its card is
  transformed for the open transition, which makes it the containing block for
  the picker's `fixed` positioning — so both opened behind, and the picker was
  laid out inside the card instead of the viewport.

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

* fix: check deploy rights per item before prompting to update the dev workspace

* fix: read the compare page link before closing the dev-workspace prompt

The link is derived from the request the prompt is answering, so closing first
left an empty string to navigate to: refusing users saw the dialog dismiss and
stay put, with no way through to the compare page.

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

* fix: keep the new-tab promise and speak up when a popup is blocked

Three defects found by successive cold reviews of the click-time resolution
added earlier in this branch, each one only reachable once the previous fix
existed:

- Safari refuses `window.open` from any promise continuation however fast it
  resolves, so the tab the editor dropdown opens after its existence probe
  never appeared there. `claimTab` takes the tab inside the click's own
  transient activation and points it at the answer once it lands, releasing it
  when there is nothing to show.
- That left the two halves of the same action disagreeing: the entry promises
  never to navigate the editor away, but when the item turned out to be missing
  the prompt took over and navigated in place. The request now carries
  `openInNewTab`, and every destination the prompt can reach honours it.
- With popups blocked the fallback called `window.open` without checking, so a
  successful deploy closed the prompt and did nothing, silently. It now names
  what it could not open.

`openEditInFork` also takes the workspace explicitly. The four editor dropdowns
computed their label from `opWorkspace` but resolved the action from the
navigation store, and `prodWorkspaceId` feeds `deployItem({ workspaceFrom })` —
so a session pane would have deployed from the wrong workspace.

`checkPathWritePermission` is exported with an injectable folder probe and
covered by table-driven cases, chiefly to pin its two fail-open branches, which
otherwise read as dead code inviting deletion.

The two unrelated whitespace hunks in ScriptBuilder.svelte are the repo's
format-on-save hook fixing pre-existing violations in a file this touched.

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

* fix: create the dev workspace's missing folder without overwriting it

`ensureFolder` delegated to the shared `deployItem`, which re-probes and
switches to `updateFolder` when the folder turns out to exist. Nobody asked for
that folder to be deployed — it is created only so the item has somewhere to
land — so a folder created between the two probes had its owners, ACL, summary
and labels silently replaced with the source workspace's. Creating is now
create-only, and losing that race counts as success: the folder exists, which is
all the caller needed.

The same delegation dropped `default_permissioned_as` and `labels`, which the
shared folder deploy does not send. A folder copied without its create-time
identity rules applies none, so an item landing inside it with no on_behalf_of
of its own resolves to whoever deployed it rather than to the principal the
source folder would have chosen — the exact substitution the rest of this branch
exists to prevent. Both are now carried across.

Also check `window.open` in the no-dev-workspace branch of `openEditInFork`. The
branch beneath it already reported a blocked popup; this one returned as if it
had opened something.

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

* fix: translate copied folder identity rules into the target workspace

A `u/<username>` names a workspace-local account, so copying a folder's
`default_permissioned_as` verbatim was wrong in two directions: the same
username in the dev workspace can be a different person, who would then be
granted the item; and a username with no account there at all passes the
folder-create check, which is structural, only to fail every subsequent item
deploy on the existence check, including the retry — the folder now exists, so
`ensureFolder` short-circuits and the deploy fails identically, with no way out
of the prompt.

Rules are now resolved source username -> email -> target username, since email
is the only identifier stable across workspaces, and a rule whose principal has
no account in the target is dropped rather than carried. Dropping one makes the
copied folder less restrictive than its source, which is not something to
discover later from an item running as the wrong user, so it is reported.

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

* fix: refuse to overwrite a concurrent item, and translate every folder principal

Four findings from CI review, all on the implicit half of this flow — the writes
the user did not explicitly ask for.

The item write is now create-only. The shared `deployItem` re-probes and silently
switches to an update, so the caller that acts on an item being *absent* could
still overwrite whoever landed it between the two probes. Rather than
reimplementing the per-kind deploys, the frontend's own provider refuses exactly
the three writes that branch reaches for — `updateFlow`, `updateApp`/
`updateAppRaw`, and a `createScript` carrying a `parent_hash`, which is what
makes an otherwise identical create an update. A refusal reports `conflict`, and
the prompt opens their version instead of replacing it.

Folder principals are translated rather than copied. `u/<username>` is
workspace-local, so a verbatim copy either names nobody or names a different
account that happens to share the username. Users now resolve source username ->
email -> target username, and the two kinds of unresolvable principal are
separated because they fail differently: an owner or ACL entry is dropped, which
can only narrow the folder and leaves the creator owning it; an identity rule
refuses the copy outright, because dropping it runs the item as the deployer and
keeping it creates a folder the server then rejects every deploy into.

Groups resolve against `listGroups` rather than `listGroupNames`, which unions in
instance groups that folder rules do not resolve against — a same-named instance
group would otherwise let an unusable rule through.

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

* fix: read every page of workspace groups before judging a folder principal

`listGroups` paginates, and the `perPage: 100` it was called with is narrower
than the server's own default of 1000 — so a group past the first page read as
having no account in the target. Since an unresolvable identity rule now refuses
the whole folder copy, that turned into a refusal naming a group that does
exist, and an owner or ACL entry on a later page was dropped silently. Read
until a page comes back short, with a size check as the backstop for a server
that ignores `page`.

`list_users` is unpaginated, so the user half of the same lookup was never
affected.

Also move `makeProvider`'s doc block back onto `makeProvider`; adding
`DeployConflict` had left it documenting the type instead.

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

* docs: reattach principalTranslator's doc block to principalTranslator

Adding `workspaceGroupNames` above it left the block documenting the helper,
the same way adding `DeployConflict` had displaced `makeProvider`'s.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:21:14 +02:00
Ruben FiszelandClaude Opus 5 221566d282 feat: let the database manager run its jobs on a custom worker tag (#10516)
* feat: let the database manager run its jobs on a custom worker tag

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

* fix: ignore a superseded database load, share the tag button between drawers

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

* fix: share the database worker tag override across mounted drawers

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:25:41 +02:00
Ruben FiszelandClaude Opus 5 ea4f3ecc6e fix: open an AI session from the editor bar's AI button, on the step (#10504)
* fix: open an AI session from the editor bar's AI button, on the step

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

* fix: withhold the session hand-off under disableAi, forward button styling

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

* fix: flush the code editor's pending keystrokes before opening the session

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:11:07 +00:00
7e1c1fa3a4 feat(apps): use the windmill-client SDK from raw app frontend code (#10377)
* feat(apps): use the windmill-client SDK from raw app frontend code

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

* fix(apps): bound the raw app SDK token to deployed runnables

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

* fix(apps): deny dependency jobs and survive a failed SDK mint

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

* fix(apps): confine the SDK token's users scope to the viewer's identity

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

* docs: describe the full raw-app SDK sentinel narrowing

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

* fix(apps): deny workflow-as-code replay for raw app SDK tokens

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

* fix(apps): deny preview-flow restart replay for raw app SDK tokens

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

* fix(apps): re-prompt when an app widens its SDK scopes mid-consent

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

* feat(apps): support the frontend SDK in sandboxed raw apps

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

* fix(apps): hand the sandboxed SDK token over only once per loaded document

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

* fix(apps): bind the sandboxed SDK handoff to the document we loaded

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

* fix(apps): use an unguessable nonce for the sandboxed SDK handoff

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

* fix(apps): reply to the sandboxed SDK handshake over its own port

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

* fix(apps): answer the raw app handshake only over a transferred port

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

* fix(apps): set frontend_sdk_scopes in the S3-gated policy literals

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

* docs: describe the sandboxed wrapper's credential as it now works

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

* ui nit

* feat(apps): make the frontend SDK work in the raw app editor preview

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

* fix(apps): guard the preview token mint and drop superseded responses

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

* fix(apps): refuse job tokens on every raw app SDK mint path

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

* docs: correct the mint caller list and the preview retry rationale

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

* fix(apps): apply the consent response's render mode before rendering

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

* fix(apps): restart the viewer when a redeploy changes the render mode

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

* fix(apps): restart on every render-mode change, not just the first

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

* fix(apps): clear the preview's SDK credential when scopes go away

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

* fix(apps): remove window.process in the preview instead of blanking it

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

* docs: cut the raw app SDK comments down to the invariant

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

* refactor(apps): use randomUUID for the raw app handshake nonce

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

* fix(apps): re-read the render mode before rendering without a token

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

* fix(apps): make the raw app handshake nonce unguessable again

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

* chore: pin the EE ref to a commit that builds against this OSS tree

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

* refactor(apps): authenticate the raw app preview by session instead of a token

The editor preview is same-origin and unsandboxed, so app code there already
holds the editing user's session cookie. Minting a scoped bearer for it added
an endpoint and a portable 12h credential without containing anything.

Inject only BASE_URL and WM_WORKSPACE: `windmill-client` falls back to
credentialed same-origin requests when it finds no token, so the SDK runs as
the editing user. Drops POST /apps/preview_sdk_token and the mint/race
handling in the editor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* fix(sdk): send credentials only outside the browser

The API answers `Access-Control-Allow-Origin: *` and never sets
`allow_credentials`, so a credentialed cross-origin request fails before the
bearer is read — which is what a sandboxed raw app issues. Keying this on the
browser rather than on `WM_TOKEN` leaves non-browser callers byte-identical,
and browsers keep sending cookies same-origin through fetch's own default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* remove windmill-client from templates

* fix(sdk): drop credentials only for raw app bundles

A sandboxed raw app calls the API from an opaque origin, and the API answers
`Access-Control-Allow-Origin: *`, which a credentialed request can never pair
with. Gate on WM_RAW_APP, set by the two places that build a raw app's
`window.process.env`, so every other windmill-client consumer is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* feat(apps): make frontend SDK access sandbox-only

An unsandboxed bundle runs same-origin with the viewer's full session, so a
consent prompt there implies a boundary that does not exist and the token adds
nothing it could not already do. Advertise scopes and mint only when isolation
is on; turning the toggle off clears the declared scopes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* Revert "refactor(apps): authenticate the raw app preview by session instead of a token"

This reverts commit 81905e455b, restoring POST /apps/preview_sdk_token.

Session auth gave the preview the editing user's full permissions and worked
regardless of policy, so an app that would 403 for a viewer — or that declares
no scopes at all — ran fine in the preview and broke only once deployed. The
preview now takes the same credential as a deployed app, gated the same way:
sandbox off or no scopes means no env at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* docs(apps): state the sandbox-only SDK contract in the public schema

The Policy and EmbedTokenResponse descriptions still promised a token to any
raw app with non-empty scopes, and said raw apps skip tokens entirely. Point
authors at adding windmill-client themselves too, since the starter templates
no longer carry it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* fix(apps): drop the preview token before minting its replacement

A mint is asynchronous, so clearing the env only on the empty-scope path left
the running preview — and any build fed meanwhile — holding scopes the policy
had just removed, or a token for the workspace just left, for as long as the
request took.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* fix(apps): restart the preview realm when its credential changes

Re-feeding the build resets the preview's DOM but keeps its JavaScript realm,
so the previous bundle's timers, listeners and pending callbacks went on using
the client they imported — and the token it captured at module load — after the
policy dropped it. Reload both shells instead; each replays the build on its
way back, so only the new realm survives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* fix(apps): start the preview once per credential change

Restarting the realm made its shell replay the build immediately, so a delayed
mint ran the app once tokenless and again tokenful — mount-time side effects
twice per scope or workspace change. Hold the build back until the mint
settles: the shell comes back blank and whichever finishes last starts the app.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* fix(apps): wait for the detached preview shell before replaying

Its reload was only initiated, never awaited — unlike the inline iframe it had
no readiness flag — so a mint settling first posted the build to the retiring
document, which then ran alongside the replacement shell's own replay. Track
readiness from both paths that announce it: `load` for a freshly opened window,
`appPreviewReady` for a reload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPCW1WB5QeYrgJmgwcywNA

* chore: update ee-repo-ref to 99e143fa1e2e6c33b3525366a5afe48f7a4f020e

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

Previous ee-repo-ref: 609e197fbc08f1ce83dd86f816748cc19d213f77

New ee-repo-ref: 99e143fa1e2e6c33b3525366a5afe48f7a4f020e

Automated by sync-ee-ref workflow.

* nit better description

---------

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-08-03 19:31:57 +00:00
Ruben FiszelandClaude Opus 5 eca24bdfb5 fix: report a missing worker tag instead of spinning in data table UIs (#10456)
* fix: report a missing worker tag instead of spinning in db/datatable UIs

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

* fix: cancel the unpickable job, confirm the tag lookup, back off the poll

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

* fix: confirm an unserved tag over ~90s and never hide a failed refresh

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

* fix: leave the job queued so the autoscaler still sees the backlog

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

* fix: cancel writes before reporting them, leave reads queued

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

* fix: read the write's terminal state instead of trusting the cancel request

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

* fix: never abandon a write, explain the wait instead of cancelling

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 13:29:07 +02:00
Ruben Fiszel 57b59a8ec8 pasting YAML over existing code fails to apply (#10453)
* fix: sync SimpleEditor code on the leading edge of a change burst

* style: condense change-debounce comment to the 4-line rule

* fix: scope leading code sync to the YAML drawers that gate on it

* fix: apply leading code sync to the remaining code-gated editors

* fix: reject OpenFlow YAML without value.modules instead of crashing the editor
2026-08-02 00:49:59 +02:00
Ruben FiszelandClaude Opus 5 9c37b0217c fix(cli): compile runes in .svelte.ts / .svelte.js modules (#10400)
* fix(cli): compile runes in .svelte.ts / .svelte.js modules

The svelte plugin only ran on `/\.svelte$/`, so a rune module like
`lib.svelte.ts` was bundled as plain TypeScript: the types were stripped
and `$state(0)` survived as a call to an undefined global, blowing up at
runtime with "ReferenceError: $state is not defined".

Route those files through `compileModule`. It parses with plain acorn and
chokes on TypeScript, so types come off first via esbuild's transform —
which is what vite-plugin-svelte gets for free by running after Vite's own
esbuild transform.

`wmill app dev` picks this up too; watch mode builds its plugin list with
the same `createFrameworkPlugins`.

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

* chore: pin ui_builder to 1ffb28e

Picks up the matching rune-module fix in the in-editor builder
(windmill-labs/windmill-code-ui-builder#24), so `.svelte.ts` modules
compile in the editor as well as through the CLI.

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

* fix(cli): compile raw apps with the app's own svelte compiler

Svelte 5.52.0 moved delegated event handlers off `element.__click` onto a
Symbol-keyed map. A raw app supplies its own Svelte *runtime* via
package.json, but `import("svelte/compiler")` resolves against the CLI,
whose own svelte floats independently — so the two can land on opposite
sides of that change and the app builds, renders, and has every
onclick/oninput silently dead.

Resolve the compiler from the app's node_modules instead, so compiler and
runtime are the same install by construction, and raise the CLI's own
floor past the break for the fallback path.

Also pin ui_builder to 013bf67, which carries the matching fix for the
in-editor builder (windmill-labs/windmill-code-ui-builder#25), and move
the Svelte raw-app template onto the same range. Those two go together:
the new builder rejects a runtime that sits on the far side of the ABI
break from its compiler.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:13:16 +02:00
bd246644c9 feat: add github dark mode variant switchable in user settings (#10002)
* feat: add github dark mode variant switchable in user settings

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

* feat: boot the UI Builder iframe into the current theme

Seed the UI Builder iframe URL with the live dark/variant state
(`?dark=&variant=`) so the VS Code workbench boots straight into GitHub
Dark / Nord / Light instead of flashing the dark default until the host's
`setDarkMode` message lands. Read from the <html> classes rather than the
reactive state so the src is computed once at mount — a later theme toggle
still updates the workbench via postMessage and does not reload the iframe.

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

* feat: align github-dark surfaces to GitHub Primer's elevation scale

Snap the invented mid-greys to Primer's real dark canvas scale
(inset=neutral.0, default=neutral.1, muted=neutral.2). Recessed surfaces
now use true inset (#010409) instead of washed-out half-steps that sat in
no-man's-land between inset and the page; raised surfaces align to muted.

- surface-secondary  #0b0e13 → #010409  (true inset)
- surface-input      #0c0f14 → #010409  (true inset)
- surface-disabled   #0b0e13 → #151b23  (muted)
- surface-tertiary   #161b22 → #151b23  (Primer neutral.2)
- component-virtual-node #161b22 → #151b23
- surface-selected   #21262d → #212830  (Primer neutral.3)

surface-sunken (#010409) and surface-primary (#0d1117) already matched
Primer inset/default exactly.

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

* fix: keep github-dark inputs subtle instead of full inset

surface-input at true inset (#010409) made form fields read as recessed as
the sidebar rail — too much contrast against the canvas. Inputs aren't
sunken, so keep the subtle #0c0f14 (a hair below the page) while the
recessed panel surfaces stay at inset.

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

* chore: bump UI Builder pin to the github-dark build (76ee616)

windmill-code-ui-builder PR #16 is merged and published to R2, so pin the
artifact to that build. The embedded raw-app editor now renders GitHub Dark
(and honors the `variant` message) natively from the pinned build — no local
swap needed. Reword the variant comment now that the dependency is resolved.

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

* fix: address codex review nits on the github-dark theme

- UserSettings: re-read the dark variant when the settings drawer opens, so a
  change made through one mounted instance (page-local drawer) isn't shown
  stale by another (the always-mounted layout instance).
- PublicApp / OAuth login callback: restore the `github-dark` variant class,
  not just the base `dark` class. These routes bypass the (root) layout, so
  they previously fell back to the default dark palette despite the saved
  preference.
- RawAppEditor: condense the two new theme comments to the durable constraints
  per AGENTS.md (drop narration and the ephemeral artifact-pin history).

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

* fix: address CI review findings on the github-dark theme

- Move the hand-authored `github-dark` token set out of the Figma-generated
  tokens.json into githubDark.json, merged back in at the two consumers
  (tailwind.config.cjs before the rgb pass, utils.ts) so a Figma re-export of
  tokens.json can no longer drop the set and crash the build.
- Restore the default-dark sidebar divider to #374151 — the PR must leave the
  Default variant unchanged; only the github variant adapts it to border-light.
- Extract the `github-dark` DOM-class read into getAppliedDarkModeVariant() and
  reuse it across RawAppEditor and vscode.ts; drop the redundant `void darkVariant`.
- Override the Monaco popup vars (suggest/hover widgets) in the github variant so
  they match the GitHub palette instead of VS Code's greys.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-07-28 18:34:41 +02:00
Diego ImbertandClaude Fable 5 3c2dab9f8f fix(apps): stop cross-origin isolating the raw app viewer (#10370)
* fix(apps): stop cross-origin isolating the raw app viewer on page reload

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

* fix(apps): shed cross-origin isolation when leaving the raw app editor

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

* chore(apps): address review nits on COEP scoping

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:41:08 +02:00
GuilhemandClaude Opus 5 8a96e3a4ec fix: raw apps with no stylesheet were permanently un-deployable (#10364)
* fix: raw apps with no stylesheet were permanently un-deployable

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

* fix: keep js strict when defaulting the raw app bundle css

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

* test: drop ephemeral narration from raw app bundle regression test

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

* test: pin the extension each raw app bundle half is fetched under

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:32:57 +02:00
Ruben Fiszel a8ef98edff feat: build a React raw app from the script and flow detail pages (#10337)
* feat: build a React raw app from the script and flow detail pages

* fix: keep generated raw-app state and setter names unique

* fix: make the generated app readable on dark and handle labeled enums

* fix: reserve the undefined binding in generated raw apps

* fix: mask password args, keep __proto__ args, and enforce required inputs

* fix: quote non-identifier arg names, preserve JSX entities, support resource args

* fix: JSON-quote generated arg keys and start resource fields empty

* fix: render array enums as multi-selects and let optional enums be omitted

* fix: stop the generated template naming Math/Array and omit untouched optional json

* fix: enforce required on array-enum multiselects
2026-07-27 12:53:43 +02:00
Ruben Fiszel e80fee86b3 feat: record and replay raw app sessions step by step (#10318)
* feat: record and replay raw app sessions step by step

* fix: address review findings on raw app session recorder

* fix: stamp replay target before pruning the snapshot clone

* fix: redact step metadata, lock down replayed frames, fix control pre-state

* feat: add a checkpoint timeline to the app recording player

* fix: parser-based replay CSP, fold label clicks, drop stale frame indices

* fix: scrub redacted attributes, keep scroll, neutralize replay navigation

* fix: bound replay payloads, strip namespaced nav links, keep control pre-frames

* fix: strip SMIL navigation, redact metadata sources, capture pre-edit on beforeinput

* fix: redact template content, drop shadow templates, make replays inert

* test: pin snapshot redaction and replay sanitization with DOM tests

* fix: allow-list no-record attributes and cover a marked document root

* fix: classify input types positively so pickers get pre-change frames

* fix: one step per control interaction and bound step metadata

* fix: keep button inputs recordable and coalesce only continuous controls

* fix: no frames for coalesced repeats and drop inline styles when redacting

* fix: fold only the label's own click and keep marked stylesheets out

* fix: keep label-forwarded and radio-group pre-frames, fold submitter clicks

* fix: bound key pre-frames to their gesture and clear ancestor pointer frames

* fix: age-bound pre-frames and treat a radio group as one target

* fix: consume pre-frames per interaction and coalesce on the browser repeat flag

* fix: spend only the pre-frame a step actually used

* fix: settle a step from its successor's pre-state and drop stale pointer frames

* fix: bound remote frame payloads and snapshot stylesheets as rendered

* fix: let a control change spend its own frame and dedupe Enter activations

* fix: record Escape on controls and drop disabled stylesheets

* feat: collapse the replay step list by default behind a toggle

* fix: neutralize disabled sheets in place and fold Enter submissions

* fix: withhold redacted control state, fold key repeats, validate remote metadata

* fix: drop noscript markup and fold implicit form submissions

* fix: mask a select whose chosen option is redacted

* fix: mask redacted select choices before the clone diverges

* fix: run clone-paired passes before removals and fold only Enter submissions

* feat: record a raw app demo from the publish flow instead of the viewer

* fix: wait for in-flight runnable jobs before settling a step

* feat: record from the editor menu and replay publicly at /replay

* feat: export the app recording player and its loader for the hub

* feat: publish from folders only, drop iframe sharing

* fix: observe runnable responses where they land and mount the hub recording route

* fix: respect the app's sandbox opt-in when recording a session

* fix: let stop wait for the runnable the last step is still running

* fix: filter redacted class/id to styled tokens and gate publish on admin

* fix: drop marked sheets from the token vocabulary and bound the replay error

* test: pin the remote app-recording validator

* fix: carry in-flight runnables across a reload and fold held keys into one step

* fix: bind runnable responses off the request and honor base in the replay handoff

* fix: close the settling step when a new fill starts and always re-read stylesheets

* fix: empty the no-record marker so it carries nothing of its own

* fix: decode css escapes so utility classes survive redaction

* fix: read keyDriven from the frame the change starts from

* docs: condense recorder comments to the invariant each protects

* fix: rewrite only real url() tokens and accept leading css escapes

* feat: play flow, script and pipeline recordings on the public /replay page (#10327)

* feat: play flow, script and pipeline recordings on the public /replay page

* fix: render a recorded approval result inert while replaying

* fix: bound an asset sample's cell product and validate recording headers

* fix: make a replayed approval step inert and bound nested recording structures

* fix: stop recorded markup from fetching and bound flow/script render trees

* fix: gate recorded markdown at its renderer and close remaining render-budget gaps

* fix: replace per-key render caps with one structural budget per recorded value

* fix: bound component fan-out and text alongside the structural budget

* fix: make component fan-out cumulative and cap the parsed data-test checklist

* fix: bound the whole recording, graph contents, metadata strings and timer bursts

* fix: keep the published loader path, charge object keys, refuse huge serialized fan-out

* fix: cap flat maps a renderer turns into rows (args, schema properties)

* fix: refuse structure hidden past the depth ceiling and bound errored samples

* fix: count array-shaped argument collections against the row cap

* feat: paint canvas pixels into the snapshot

* fix: budget canvas encoding per snapshot and bound the unknown-kind error

* fix: cap flow graph overlay fan-out and condense budget comments

* docs: teach the raw-app prompt about data-wm-no-record
2026-07-26 11:51:59 +02:00
48618cff8c feat: Add image when publishing a project (#10310)
* refactor(hub): remove per-item Publish to Hub entry points

Publishing to the Hub now happens exclusively through the folder-level
deploy-to-hub flow (/folders). Remove the standalone entry points:

- script detail page menu item (and the SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB
  const that gated it)
- script list row dropdown item
- raw app editor menu item, its zip-download drawer and publishToHub()
- long-dead commented block in AppEditorHeader

Also drop the now-orphaned URL helpers (scriptToHubUrl, flowToHubUrl,
appToHubUrl, rawAppToHubUrl) from lib/hub.ts.

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

* feat(hub): upload a custom project logo from the deploy-to-hub drawer

Add a Logo field to the bundle metadata form: a drag-and-drop dropzone
(png/svg, 512KB client-side cap mirrored server-side by the Hub) that
turns into a live replica of the Hub project card once an image is
picked, so the logo can be judged in context before publishing. The
logo is pushed after the draft's items/migrations via the new
POST /projects/{slug}/logo proxy in hub_publish.rs (slug validated by
construction, `logo: null` forwarded to clear). Leaving the field empty
never touches the Hub's existing logo, so re-publishing a bundle keeps it.

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

* fix(hub): logo removal, safer mime inference, explicit clear semantics

Review follow-ups on the project logo upload:

- Removing a published logo is now possible: hubLogo is three-state
  (undefined = leave the Hub's logo alone, null = clear on publish,
  object = upload). Rehydration reads has_logo so the drawer shows a
  "Remove on publish" affordance when the Hub already has one, with an
  undo banner before publishing.
- hub_publish.rs uses a double-Option for the logo field: a missing
  `logo` key is now a 400 instead of being serialized as `logo: null`,
  which the Hub interprets as an explicit clear — POSTing `{}` can no
  longer silently delete a project's logo.
- Client mime inference prefers the browser-reported file.type over the
  filename extension, so a PNG misnamed *.svg no longer produces a
  broken preview and a guaranteed server-side sniff rejection.

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

* Update frontend/src/lib/components/workspaceSettings/deployToHubSession.svelte.ts

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

* Update frontend/src/lib/components/workspaceSettings/DeployToHub.svelte

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

* fix(hub): validate logo size/mime/base64 in the proxy, document the endpoint

- Enforce the logo constraints in windmill-api itself instead of relying
  on the browser and remote Hub: a route-level DefaultBodyLimit sized
  for a max logo in base64 (+JSON envelope) overrides the global request
  limit, and the handler validates the mime allowlist, base64 alphabet
  and decoded length (512KB cap) before anything is forwarded.
- Add /w/{workspace}/hub/projects/{slug}/logo to openapi.yaml (with the
  ProjectLogoBody schema) and regenerate the frontend client.
- Drop a narrating comment on the hidden file input.

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: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-07-24 18:19:25 +02:00
Ruben FiszelandClaude Opus 4.8 d03810276c remove collaborator presence badge from session preview (WIN-2222) (#10259)
* fix(ai-sessions): hide collaborator presence badge in script preview

The workspace-wide presence layer (MultiplayerMenu) auto-connects under an
EE license and broadcasts each user's window.location.pathname. The
<Awareness /> badge in ScriptBuilder's header renders anyone whose path
matches the current pathname. In the AI session preview the editor is
embedded in the /sessions page, so presence collides on that single coarse
URL and paints a meaningless badge (often the current user themselves) on a
brand-new draft where no one is co-editing.

Suppress the badge whenever ScriptBuilder runs inside the session pane,
reusing the existing inSessionPane context flag.

Fixes WIN-2222

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

* fix(ai-sessions): hide presence badge in flow/app/raw-app session preview too

Extend the same inSessionPane guard to FlowBuilder, AppEditorHeader and
RawAppEditorHeader so the spurious collaborator presence badge is hidden in
every editor embedded in the session pane, not just scripts. The aiChatManager
context is injected only by SessionEditorTarget, so the guard is a no-op in the
standalone editors.

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 13:03:37 +02:00
GuilhemandClaude Opus 4.8 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
hugocasaandClaude Opus 4.8 7a139ab23e feat(ai-chat): image attachments and agent raw-app screenshots (#10130)
* feat(ai-chat): add image attachments and agent raw-app screenshots

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

* docs(ai-chat): generalise take_screenshot fidelity caveat

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

* fix(ai-chat): keep compaction boundary on a displayed user message

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

* fix(raw-apps): count line boxes by vertical overlap, not rect count

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

* fix(ai-chat): enforce vision gating and bound image attachments

Refuse images on known text-only models instead of warning and sending them
anyway; cap input bytes before decode; keep clipboard text when it accompanies
a bitmap; don't queue a message whose images can't ride the plain-text queue.

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

* perf(ai-chat): trim take_screenshot schema and shrink the card's copy

Move the fidelity caveat from the tool def onto the tool result: the def is
re-sent every global iteration (~258 tok), while the caveat only matters once a
capture exists. Keep a downscaled copy in displayMessages when it is actually
smaller — those are never compacted and are re-cloned on every saveChat.

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

* fix(ai-chat): carry attached images through the message queue

Enter during a streaming turn queued the text and silently dropped the images,
so the auto-send was not the message the user submitted. The queue now holds
both, moved together via takeQueue/clearQueue/restoreQueue so none of the three
flush sites, the dequeue-to-composer path, or the two conversation-switch drops
can leak one without the other.

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

* fix(ai-chat): gate screenshots on vision, narrow when the tool fires

take_screenshot buffered an image unconditionally, so a text-only model got an
image_url and rejected the turn; the attach-time check never covered it, nor a
model switched after attaching. Gate before capture and again at send.

Only reach for the tool when the user raises how the app looks, rather than
after every UI edit.

A collapsed preview keeps the iframe mounted at zero width, passing the ready
checks and then failing inside the rasteriser as '[object Event]'. Name it.

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

* fix(ai-chat): hold sending while attachments decode

addImages read the free-slot count before its await and appended after it, so a
send during the ~50-800ms decode cleared images while the closure still wrote to
them, landing the picture on the following message; two drops also claimed the
same slots and could pass the cap. Reserve slots up front, block sending until
they resolve, and show a placeholder so the held send is explained.

Keep only a bounded copy in the transcript: displayMessages are never compacted
and are re-cloned on every save. Measured 6.1x smaller per attachment.

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

* fix(ai-chat): route screenshots to the visible tab, resend full-res on retry

Every mounted raw-app editor claimed the runtime's single screenshot slot, so
take_screenshot could capture a background tab's app; ownership now follows the
visible tab and only the owner releases it.

restartGeneration resent displayMessages' images, which became a 384px thumbnail
when the transcript copy was bounded — retries downgraded the model's own input.
Recover the sent parts from the API message instead.

Move modelSupportsVision to modelConfig: it was untestable behind lib.ts's monaco
import chain, and the denylist missed bundled text-only defaults (Groq/Together
Llama 3.3, Foundry Phi-4 and Mistral-Large). Llama 3.2 and Phi-4 split by variant,
so both are matched narrowly. Pinned against the shipped defaultModels.

Decode attachments one at a time and derive the preview from the bounded copy: a
12MP bitmap is ~48MB and the batch was held live at once, decoded twice each.

The attach tooltip claimed nothing is uploaded, which is untrue for images.

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

* fix(ai-chat): keep images out of text-only turns and bound the queue

The vision gate only dropped the current turn's images, so history's image parts
still went out after a switch to a text-only model and failed the request; strip
the outbound copy instead, leaving history intact for a switch back.

queueMessage had no cap, and each queued send clears the composer for another
eight, so repeated sends stacked an unbounded batch into one message.

Editing a message resent displayMessages' bounded copy, downgrading the model's
own input; retries recovered the full-size one but then re-persisted it at full
resolution. storedImages pairs the API message with its transcript entry so both
paths resend the original and re-persist the bounded copy.

Reserve image slots before awaiting text attachments: the gap left sending
enabled with an image pending, measured ~90ms for a 40-file drop, now ~8ms
regardless of batch size.

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

* fix(ai-chat): treat deepseek-v4 as text-only

deepseek-v4-pro ships as a bundled default and the gate let images through to
it, so an attachment would fail the turn. DeepSeek's vision line is deepseek-vl.

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

* fix(ai-chat): drop a rejected image instead of wedging the conversation

A provider that refuses an image leaves it in history, so every later turn
resends it and fails identically: the chat is stuck until the user edits the
message or starts over, and Retry re-sends the same image. The vision gate only
knows the models we ship, so this is the net for the rest. Strip the parts on an
image-related rejection and say so; unrelated failures keep the image.

Verified at the wire that no provider rejects a base64 data URL: anthropic
(source.base64), openai/gpt-4o (input_image), googleai and aws_bedrock/claude
(image_url passthrough) all 200 and read the image.

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

* fix(ai-chat): match text-only models exactly, from provider API docs

The gate guessed by substring over model names, which answers the wrong
question. What matters is whether a provider's API accepts image parts, not
whether the model can see: DeepSeek V4 ships vision in its chat product that its
API has no content type for, and o3-mini gained vision in ChatGPT the API never
exposed. Neither is inferable from a name.

Substrings also block working models. 'mistral-large' matches Mistral Large 3,
which takes images; 'phi-4' matches Phi-4-multimodal, which does too. A wrong
entry blocks with no override, while a missing one costs a turn and recovers via
the rejection path, so the list is now exact ids only, each backed by a provider
doc.

Verdicts verified against provider API docs rather than recall. Live-checked
where a doc was contradicted: Bedrock's compatibility matrix claims no Anthropic
model is served over chat completions, but it serves images fine.

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

* fix(ai-chat): stop retry resurrecting a rejected image

The rejection fallback strips the image from history but leaves the bubble's
thumbnail so the user can still see what they sent. storedImages fell back to
that thumbnail when the API message had no parts, so Retry re-attached the very
image the provider had just refused and failed identically — the conversation
stayed wedged through the one control offered to escape it.

Found by retrying in the UI; unit tests, wire tests and four review passes all
missed it, since it only exists between two separate fixes.

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

* fix(ai-chat): harden image rejection recovery and drop-path attachment

* fix(ai-chat): fix image drop race, mid-turn vision gate, retry aliasing

* fix(ai-chat): key vision denylist by provider, flatten alpha before jpeg

* feat(ai-chat): offer take_screenshot on chromium only, ask for one elsewhere

* feat(ai-chat): image-only sends and click-to-expand image previews

* fix(ai-chat): capture screenshots at 2x and expand tool images full-res

* feat(frontend): expandable image previews in composer and result views

* fix(ai-chat): image-only send edge cases from review round

* fix(ai-chat): keep image-only drafts on rollback, track failing model id

* fix(ai-chat): gate rejection recovery on the failing iteration's model

* refactor(ai-chat): record iteration model via onBeforeIteration, trim tests

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

* fix(ai-chat): restore composer draft when beforeSend preflight fails

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

* fix(ai-chat): bound cumulative outbound image bytes per request

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

* fix(ai-chat): make the image byte bound part-granular so over-cap turns keep a subset

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

* fix(ai-chat): evict newest-first within a message in the image byte bound

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

* fix(ai-chat): prune over-cap images from stored history, not just requests

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

* fix(ai-chat): bound history at every save boundary, keep thumbnail pairing across eviction

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

* fix(ai-chat): slot-align storedImages so the bubble expands the right image after eviction

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

* fix(ai-chat): match rejection keywords as whole words so provisioning errors keep images

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

* fix(ai-chat): match input_image rejections, restore images refused by non-GLOBAL modes

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

* fix(ai-chat): defer non-GLOBAL image refusal restore past the composer clear

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

* fix(ai-chat): persist full tool screenshots for post-reload expansion

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

* refactor(ai-chat): persist chat images out-of-band via blob-store refs

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

* fix(ai-chat): scope image blobs per chat and stop cap-eviction rotation

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

* fix(ai-chat): keep blob-cap chronology across drop-oldest compaction

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

* refactor(ai-chat): derive blob eviction from the saved record, not write times

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

* fix(ai-chat): serialize chat history DB writes per manager

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

* fix(ai-chat): pin queued history writes to the enqueue-time user database

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

* fix(ai-chat): delete stale image blobs only after the chat record commits

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

* fix(ai-chat): don't double-restore a queued image-only draft on vision refusal

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

* fix(ai-chat): label image-only chats and evicted image-only bubbles

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

* fix(ai-chat): keep the in-memory chat mirror hydrated for DB-less sessions

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

* fix(ai-chat): converge the chat mirror to refs after a successful DB commit

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

* fix(ai-chat): guard mirror convergence against rewinding newer saves

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

* fix(ai-chat): invalidate pending convergences on identity re-init, keep retry image names

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

* fix(ai-chat): bound the screenshot raster before rasterization

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

* refactor(ai-chat): drop the no-IndexedDB in-memory image fallback

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 19:36:51 +02:00
hugocasaandClaude Opus 4.8 24750e6ef1 fix(raw-apps): prevent and surface the silent blank screen from an unmounted #root (#10150)
* fix(raw-apps): prevent and surface the silent blank screen from an unmounted #root

An `index.tsx` written as a bare `export default function App() {...}` with
no mount call builds and runs without throwing: the preview executes the
bundle against an empty `<div id="root">` and auto-renders nothing, so the
JSX never runs, nothing reaches the console or the runtime-error overlay,
and the app is blank with no diagnostic.

Prevent it: the raw-app system prompt and the in-chat app prompt now state
that `index.tsx` is the mount entrypoint, show the mount shim for React,
Svelte and Vue, and call out that a bare component fails silently.

Surface it: when a build still mounts nothing, the preview harness posts
`emptyRender` and the editor shows an error overlay naming the missing
call. The harness reports only when nothing is on screen AND the app never
looked `#root` up, so an app that mounted but paints nothing yet (a fetch
in flight, an unresolved Suspense) is never flagged; `renderAppeared`
withdraws the overlay if a deferred mount lands late.

The handlers stay dormant until the builder tarball that emits these
messages is pinned via `ui_builder_artifact.json`.

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

* chore(raw-apps): pin the UI builder artifact that emits emptyRender

Activates the "Nothing was mounted" overlay: the pinned tarball predates
the harness change, so the host handlers were dormant until now.

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

* chore: drop a screenshot accidentally committed at the repo root

Not referenced anywhere; the PR's screenshots are hosted externally.

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

* fix(raw-apps): make the unmounted-app guidance framework-aware

The detector fires for every raw-app framework, but the overlay and the
prompts named React's `index.tsx` and `createRoot` unconditionally. Svelte
and Vue apps mount from `index.ts` via `mount` / `createApp`, so the
guidance pointed at a nonexistent entrypoint and an unavailable API.

Derive the entrypoint and mount call from the app's files, keyed off file
extensions rather than the template filenames, which users rename.

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-16 15:48:50 +02:00
03535691d6 fix(frontend): open new script/flow/app in AI session (not-found + friendly tab) (#10028)
* fix(frontend): open new script/flow/app in AI session without "not found"

"Open in AI session" on a never-deployed item opened the session preview
against the friendly live-edited path (script.path / $pathStore) instead of
the URL draft path the editor loads and saves by, so get-by-path 404'd. It
also flushed only queued autosaves, so an untouched new item — which never
triggered autosave — had no draft row to load at all.

Target the URL draft path (userDraftPath / liveEditorDraftStoragePath;
raw-app already used appPath), and add UserDraft.forcePersist to materialize
a brand-new draft in beforeOpen, gated to never-deployed items where there is
no deployed baseline to discard against.

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

* fix(frontend): label a new session preview tab by its friendly name

A never-deployed item's preview tab read draft_<uuid> instead of the typed/
auto name. The sessions page can't reactively read a runtime cell's state
across reactive roots, so the live editor (SessionEditorTarget, handed the
runtime as a prop) now stamps a transient friendlyLabel onto the tab model —
which the page does observe — via a pure draftFriendlyLeaf helper. Unifies
scripts, flows and raw apps through one path.

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

* fix(frontend): address review nits on AI-session open

- Flow drawer (FlowEditorDrawer) mounts FlowBuilder with no
  liveEditorDraftStoragePath, so gating the AI button solely on it hid the
  session entry point there; fall back to $pathStore (the pre-PR behavior for
  those deployed-flow drawers) while the main editor still prefers the URL
  draft path.
- Clear a tab's stamped friendlyLabel when it is retargeted, so a draft tab's
  friendly name no longer lingers after navigating to a plain page.
- Trim the repeated persist-hook comments to satisfy the AGENTS.md comment rule.

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: Ruben Fiszel <ruben@windmill.dev>
2026-07-10 13:39:52 +00:00
GuilhemandClaude Opus 4.8 c139eed631 fix: session preview tab labels, splitter hover, and diff-drawer sizing (#10008)
* fix: show pending friendly path in new raw app session tab

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

* fix: reveal a subtle rounded grabber on the sessions chat/preview splitter on hover

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

* fix: cap session diff blocks so each item's card fits the drawer viewport

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

* fix: floor flow diff cap at its min height and tidy diff/splitter comments

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:54:58 +02:00
GuilhemandClaude Opus 4.8 c000bbca28 fix(frontend): scope raw-app, flow and script editors to the session workspace (#10015)
* fix(frontend): scope raw-app/flow/script editors to the session workspace

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

* fix(frontend): scope flow and script editor operations to the session workspace

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

* fix(frontend): scope flow preview, inline-script creation and datatable schema to the session workspace

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

* fix(frontend): address Codex review — thread session workspace through flow resource pickers, script fetch, preview cancel/recording and path collision check

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

* fix(frontend): address Claude review — pass session workspace to preview FlowStatusViewer and align FlowChatManager guards

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

* fix(frontend): address Pi review — show acting workspace in script-not-found message and fetch picked script from it in EditorBar

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

* fix(frontend): address Codex review round 2 — thread session workspace into flow step test, raw-app inline runnable, inline editor toolbars and MCP OAuth path

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

* fix(frontend): address Codex review round 3 — thread session workspace into dynamic-input helpers and the flow-preview argument side panel (history/saved-inputs/captures)

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

* fix(frontend): address Codex review round 4 — thread session workspace into nested flow/script drawers, flow chat inputs and the flow input side tabs

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

* fix(frontend): address Codex review round 5 — thread session workspace into script-module fork/reload and key the raw-app schema cache by workspace

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

* fix(frontend): address Codex review round 6 — key the DB manager schema cache by acting workspace

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

* fix(frontend): address Codex review round 7 — thread session workspace into resource-valued arg pickers and the editor variable/resource helper drawers

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

* fix(frontend): scope the flow asset explorer's ResourceEditorDrawer to the acting workspace

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

* fix: thread acting workspace through flow asset explore controls

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

* fix: thread acting workspace through SQL REPL, secret args, helper forms, S3 inputs, saved inputs

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-09 01:53:43 +02:00
GuilhemandClaude Opus 4.8 b847ca2bc7 feat: condensed top bar for session preview editors (#10011)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:35:57 +00:00
GuilhemandClaude Opus 4.8 fb12b23e01 fix: session preview editors and picker dropdown overflow (#10010)
* fix: constrain script/flow/raw-app editors to container height in session preview

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

* fix: clip session preview picker dropdown to popover so it stops overflowing the page

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-08 13:56:31 +00:00
GuilhemandClaude Opus 4.8 95031903eb feat(sessions): v2 unified sidebar with family/fork scoping and preview router (#9816)
* feat(sessions): prototype session-mode layout wrapper (design exploration)

Do not merge — design exploration of an optional full-page 'session mode' layout for AI sessions.

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

* feat(sessions): add full-screen toggle for the session panel

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

* feat(sessions): workspace-tree rail with browse mode and collapsible sidebar

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

* feat(sessions): restore sessions page with iframe preview of current view

Roll back the session-mode layout wrapper: sessions is a dedicated /sessions
page again rather than a layout toggled over the live app. Opening a session
from a Windmill page captures that page as the session's preview target; the
page shows the chat beside a preview panel that iframes the target, with a
breadcrumb and full-screen toggle.

- Remove SessionShell wrapper and the sticky sessionLayout flag; +layout.svelte
  always renders the normal global sidebar. Sidebar components introduced
  alongside the wrapper are kept for the upcoming sidebar rework.
- sessionMode.svelte.ts: per-session preview-URL map (captureSessionView /
  sessionPreviewUrl) + withMenuHidden to drop the previewed page's own sidebar
  via the nomenubar flag.
- Drop the #content sidebar gutter (pl-12/pl-40) when the menu is hidden, so
  the nomenubar preview fills the panel edge-to-edge.
- SessionPicker: activate() navigates to /sessions; createAndOpen() seeds the
  new session's preview from the current page.

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

* feat(sessions): add exit (X) button to chat header

Add a close button at the top-right of the session chat header that leaves the
sessions page and navigates to the session's target (the previewed page), so
exiting lands on exactly what was being previewed, full-screen with the sidebar.

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

* feat(sidebar): promote workspace picker and widen the sidebar

Replace the Windmill logo header with the workspace picker so the active
workspace is the sidebar's anchor: show the workspace name (not the id) in a
stronger weight, with a down-chevron and a bottom-aligned dropdown. Add the
same dropdown chevron to every other sidebar menu trigger (Favorites, User,
Settings, secondary/Help groups) via an opt-in MenuButton option, and widen
the expanded sidebar from w-40 to w-48 (content offset kept in sync).

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

* feat(sessions): collapsible preview panel + sidebar session entry polish

Add a collapse control to the sessions preview panel (top-left, matching the
legacy editor's PanelRightClose), animated with an x-axis slide. The panes
carry no explicit size so Splitpanes auto-distributes — the chat fills the
width when the preview collapses and splits evenly when both are shown. When
collapsed, a floating "Open side panel" Button (top-right) brings it back.

Also gather the AI sessions section into the Favorites/Search container via a
new embedded mode on SessionPicker, replace the small "+" with a full sidebar
"New AI session" entry, and drop the chat header's exit (X) button.

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

* feat(sessions): split family/fork picking with a global breadcrumb

Separate workspace-family selection from fork selection. The sidebar
workspace picker now lists families (roots) only and shows the active
family name even inside a fork. A persistent `family · fork` breadcrumb
lives in the global logged layout (WorkspaceBreadcrumb, rendered via a
new AiChatLayout topBar snippet): the fork segment opens the fork picker
popover, staging a pending fork on a draft session (the old in-chat
SessionWorkspaceBar semantics) or switching workspace directly elsewhere.
WorkspaceFamilyPicker gains onRequestCreateFork to route create-fork to
the global fork modal in non-session contexts.

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

* revert(sessions): drop the global fork breadcrumb top bar

Remove WorkspaceBreadcrumb and its AiChatLayout topBar wiring; restore
the in-chat SessionWorkspaceBar for draft fork-picking and the original
WorkspaceFamilyPicker. The sidebar workspace picker stays family-only
(roots, no forks listed).

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

* feat(sessions): family/fork-scoped sidebar with scope header

Restructure the sidebar into a family-scoped region (workspace family
header → New AI session → session list) and a workspace-scoped region
(a Fork scope header → Favorites + Search → workspace items), split by a
full-width divider. The new WorkspaceScopeHeader is a full-width
root/fork picker: accent-styled on a fork (text + faded border), with a
bottom "<workspace> settings" link; picking a different fork from a
session navigates home. The family header keeps the root's color when
inside a fork, and drops "Fork current workspace" / "Workspace settings"
(now surfaced via the scope header and the bottom Settings dropdown).
The session preview header shows "family · fork <page path>".

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

* fix(sessions): drop colon from "Workspace root" scope label

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

* feat(sessions): turn the preview breadcrumb into a page router

Every breadcrumb segment now opens a drill picker that lists workspace pages
(Home, Runs, Workspace settings, …) alongside scripts/flows/apps. Picking
either steers the preview iframe without leaving the sessions page. The
non-item case resolves to the page's real name (e.g. "Workspace settings").

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

* feat(sessions): tabbed preview with mounted tabs + Home quick-access

The preview is now a tiny tabbed browser: the first tab is pinned to the
session's view, "+" opens the router picker to add more, and every tab stays
mounted (stacked + visibility-toggled) so switching preserves each page's
state. Per tab, the commanded `url` is decoupled from the observed `loc` so
in-iframe navigation never reloads the frame. Home is also pulled up as the
first quick-access item in the router picker.

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

* feat(sessions): persist preview tabs with the session in IndexedDB

Save the open preview tabs (+ active tab) onto the session record so reopening
a session restores its tabs. Write-behind is debounced since a tab's observed
location churns as the user browses; transient (unsent) sessions skip it.

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

* perf(sessions): lazy-mount preview tab iframes

Only boot a tab's iframe the first time it's activated, then keep it mounted.
Restoring a session with N saved tabs now boots just the active tab instead of
N full Windmill apps at once.

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

* feat(sessions): fold the breadcrumb picker into the preview tabs

Drop the separate family·fork/path breadcrumb bar. The active tab now doubles
as its own router picker (click it to re-point the tab); inactive tabs switch
on click. Removes the now-unused PreviewRouterSegment.

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

* feat(raw-apps): auto-compact the editor when it opens narrow

On the first measured layout, if the editor container is under 800px, drop to
the merged single-pane view and retract the file sidebar (e.g. when shown in
the narrow session preview pane). Applied once on open; the sidebar is set
without persisting so it never overrides the user's saved preference.

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

* feat(sessions): auto-refresh preview tabs after mutating chat tools

Add a tool-completion hook in the shared chat dispatcher; the sessions page
subscribes and debounced-reloads every mounted preview tab when a write/deploy/
delete tool finishes (matched by verb prefix, so read/test/navigate tools skip).

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

* fix(sessions): keep nav bar hidden across in-preview navigation

The sessions preview iframes load pages with `nomenubar=true`, but the
layout recomputed `menuHidden` from the current URL on every navigation,
so a client-side nav inside the preview (an in-page link or redirect)
dropped the flag and the global nav popped back in. Make the hidden state
sticky for the document's lifetime when running inside an iframe; the top
window is unaffected so the oauth-callback toggle still works.

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

* fix(sessions): persist hidden nav across full reloads in preview iframe

The in-memory sticky flag was lost on a full document load inside the
preview (a navigation that drops the `nomenubar` query param), so the
global nav — including the mobile burger — reappeared. Store the sticky
state in sessionStorage so it survives full reloads within the iframe's
browsing context. The top window is unaffected.

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

* fix(sessions): refuse to mount sessions UI inside a preview iframe

A preview tab navigating back to /sessions would mount another sessions
page with its own preview iframes, nesting endlessly. When the page
detects it is running inside an iframe, render a stub that breaks out to
the top-level window instead of mounting the full UI.

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

* feat(sessions): Workspace ⇄ AI Sessions mode switch + workspace-decoupled session chat

Add a route-derived mode switch that flips the sidebar rail between the classic
workspace navigation and a dedicated AI-sessions sidebar, cleanly separating
sessions from the workspace nav.

- SessionModeSwitch (Workspace | AI Sessions) in the rail; session mode is
  exactly "on /sessions", so the switch just navigates in/out (sessionSwitch).
- Session chats target their own (possibly forked) workspace via
  AIChatManager.operatingWorkspace/workspaceResolver without mutating the global
  workspaceStore; "Acting on" header strip shown once a session has started.
- Flow editor AI button becomes "Open in AI session": saves the draft, then
  opens a new session targeting the current flow.
- New sessions: no default preview (empty state instead of iframing home, panel
  collapsed); preview-panel collapse persisted per-session on the record.
- Persist nav-rail collapse (manual toggle only) and drop the editor-route
  auto-collapse that fought it.
- Smaller fork picker; add a `preview` proxy so `vite preview` reaches the
  backend for production-build demos.

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

* fix(ai-chat): replay assistant turns verbatim so thinking blocks validate

The global AI chat reconstructs each assistant turn from an OpenAI-shaped
message, keeping only the thinking/redacted_thinking blocks and re-injecting
them at the front of the content array. When a turn interleaves thinking with
the native web_search tool and ends in a tool call, this reorders the thinking
blocks and drops the server_tool_use / web_search_tool_result blocks. Anthropic
validates each thinking block's signature against the blocks that precede it in
the latest assistant message, so the replayed turn is rejected:

  400 invalid_request_error
  "messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest
   assistant message cannot be modified. These blocks must remain as they were
   in the original response."

Preserve the full `finalMessage.content` verbatim (`_anthropicContent`) and
re-emit it unchanged, instead of extracting and reordering thinking blocks. Skip
the standalone text message that the streamer emits for the same turn (its text
is already inside `_anthropicContent`). The previous thinking-only path is kept
as a fallback for sessions persisted before this change.

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

* fix(sessions): give empty-state preview picker its own open state

* feat(sessions): render preview editors as components, not iframes

Introduce a PreviewTabHost seam that routes each preview tab to either an
in-process editor (the session's script/flow/raw_app target, reusing the
existing *EditorView wrappers + shared runtime) or an iframe fallback for
pages and other items, behind a uniform reload(). resolvePreviewTab classifies
a tab from its URL + the session target.

Also intercept in-iframe navigation to an editor route (logged layout
beforeNavigate): post the target up to the sessions page, which promotes the
active tab to the live editor component, so an editor is never booted inside
an iframe.

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

* fix(sessions): drive open_preview tool through the multi-tab model

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

* docs: plan SessionPreviewTabs deep module for sessions preview tabs

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

* refactor(sessions): own preview tabs in a SessionPreviewTabs deep module

Collapse the three drifting preview-tab copies (page-local state, session
record, legacy previewUrls localStorage) into one live owner held on
SessionRuntime.previewTabs. Both the sessions page (renderer) and the
open_preview/get_preview_status tools cross it, so both sync effects and the
localStorage seed disappear; url/target writes become atomic.

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

* fix(sessions): gate the Workspace/Sessions switch behind the global-AI dev flag

The SessionModeSwitch is the only entry point into the AI-sessions
experience, so gate it on wm_dev_global_ai like the global chat and the
sessions page — otherwise the unfinished mode ships to prod.

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

* fix(sessions): pin the settings footer and normalize row text in the fork dropdown

Split the family picker menu into a scrollable body + a pinned settings
footer so the workspace-settings link stays visible while the fork list
scrolls. Give every row a uniform text-primary font-normal style (rows
were inheriting a bold 600 weight; the settings link was text-secondary).

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

* feat(sidebar): fold theme switch into the settings dropdown and keep it in session mode

Move the Switch-theme toggle into the sidebar Settings dropdown and reorder
its entries (bottom-to-top: Instance, Workspace, User). The dropdown now
renders in both navigation and session modes; session mode hides only the
workspace-settings entry (the rail's global workspace doesn't map to a
session's forked workspace).

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

* fix(fork): validate fork name/id length before creation

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

* fix(sessions): title open-in-workspace button "Open in workspace"

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

* fix(sessions): keep AI chat working when the sessions dev flag is off

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

* fix(sessions): match burger drawer width and keep it open on mode toggle

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

* feat(sessions): surface the dev-workspace badge across session workspace pickers

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

* fix(sessions): dedup navigate, sanitize hydration, cap mounted tabs

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

* feat(fork): support forks of forks via a base-workspace picker

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

* feat(sidebar): family expansion, pinned menu actions, animated popovers

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

* feat(sessions): persist unsent drafts, gate preview, loading state

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

* fix(sidebar): group fork picker on top and unfold the session list

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

* fix(sessions): capture splitter pointer so off-window release ends drag

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

* refactor(sessions): retire the pinned preview tab (dot and no-close)

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

* feat(sessions): shared open-in-AI-session button across editors

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

* fix(sessions): dedup page tabs, flush on hide, review cleanups

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

* feat(sessions): give unsent drafts a side panel, reset tabs on retarget

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

* fix(sessions): keep the session fork icon neutral except when detached

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

* fix(sessions): scope session-mode restore and transient reuse to family

* fix(sessions): preserve session mode across workspace switches

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

* fix(sessions): reconcile open session with family on workspace switch

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

* fix(sessions): lazy-load runtime in session switch to keep it node-testable

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

* feat(sidebar): add bottom brand mark and standalone workers/logs links

* feat(sidebar): add name+id copy tooltip to workspace picker

* style(sidebar): add spacing between settings and brand mark

* feat(forks): id-based fork creation, fork color theming, picker polish

* feat(forks): copy-id in session header, inert chip, fork form polish

* fix(sidebar): restore logs, help, user and leave-workspace menus

* feat(sidebar): carry active tick on collapsed family root

* feat(dev): add settings-menu kitchen sink page

* fix(sessions): fail closed for unbound persisted sessions in family scope

* fix(sidebar): keep workspace URL param in sync across switches

* feat(sessions): remove home page from preview tab navigation

* refactor(sidebar): dedupe shared helpers and address review findings

* feat(sessions): keep preview hosts alive across session switches

* feat: workspace settings links in session rail, acting badge and family picker

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

* fix: refresh session changes bar after out-of-window deploy

The session "Edits" bar re-fetched its draft list and existence checks
only on AI turn-end, tab visibilitychange, and drawer open. Deploying an
item from a full-page editor in a second browser window left the bar
stale: that tab never goes hidden, so visibilitychange never fires, and
the badge kept reading "1 draft" while opening the drawer showed no
pending change.

Add a window `focus` listener alongside visibilitychange so returning to
the session window re-syncs the bar, and refresh the dock when a badge is
clicked so the drawer always opens on fresh state.

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

* feat(sessions): show name/id copy tooltip on acting badge, drop inline copy

* fix: keep editor header cloud indicator visible at narrow widths

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

* fix(sessions): scope preview reload to the mutated item

Reloading every mounted preview tab on any mutating chat tool blank-
rebooted unrelated raw-app previews: a raw app that isn't the session's
live-editor target renders as an /apps_raw/edit iframe, and reloadAllTabs
hard-reloaded it (frame.location.reload) on every write/deploy elsewhere.

Pass the tool args through the completion listener and scope the reload:
an item-route iframe reloads only when its item was actually touched. The
changed item is args.path for workspace-path tools; the raw-app file tools
(write_app_file, …) pass a leading-'/' frontend file path and edit the
active session's target app, so scope to the target; anything else is
unresolved and reloads everything (safe fallback). Changed paths accumulate
across the 500ms debounce. Non-item pages still always reload; live-editor
slots still no-op.

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

* fix(diff): honor side-by-side/unified toggle and widen draft drawer

Monaco forces inline view below its 900px renderSideBySideInlineBreakpoint, which overrode our SIDE_BY_SIDE_MIN_WIDTH gate and made the toggle a no-op in the ~800px draft drawer. Disable useInlineViewWhenSpaceIsLimited so our width logic wins, and widen the drawer default 1200->1500px.

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

* fix(diff): vertically center the element-header icon with its path

The path renders as ExternalEditLink's inline-flex <a> in production, which sat ~2px low on the wrapper's line-box baseline. Make the path wrapper flex+items-center so the icon and path align by box.

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

* fix(diff): reflect the auto-unified downgrade in the drawer view toggle

The side-by-side/unified downgrade lived inside each DiffEditor's width gate, so the drawer toggle still showed side-by-side when the narrow column rendered inline. Measure the diff column, make the drawer authoritative (force inline when narrow), and reflect it in the toggle (unified selected, side-by-side disabled) while preserving the user's preference for when it widens again. Shared SIDE_BY_SIDE_MIN_WIDTH via diffEditorTypes.

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

* feat(sidebar): make the nav rail resizable with rem scaling

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

* fix(diff): gate Monaco auto-inline behind a prop to keep narrow diffs unified

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

* fix(ui): restore instant popover/dropdown default, opt sidebar and sessions in

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

* feat(sidebar): restore delete-forked-workspace action in the settings menu

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

* feat(tooltip): add cursor anchoring option and use it for the name/id tooltip

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

* chore(dev): remove settings-menu kitchen sink scaffolding

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

* chore(docs): remove session-preview-tabs owner plan doc

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

* chore(dev): drop vite preview-server proxy scaffolding

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

* feat(sidebar): scroll nav as one block with fade hints, pin settings to bottom

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

* fix(sidebar): guard against concurrent pointer drags leaking resize listeners

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

* fix: scope session preview, LLM proxy, and raw-app workspace switch

Address PR review findings: session preview iframes, the AI chat LLM
proxy client, and the raw-app workspace-switch guard all now resolve the
session's effective workspace instead of the global navigation workspace.

- withMenuHidden appends the session workspace as ?workspace= so preview
  iframes render fork-scoped pages against the fork, not the nav workspace.
- AIChatManager builds the proxy clients from operatingWorkspace so the
  LLM request hits the session workspace's /ai/proxy, not the global
  singleton (init'd only on global workspace changes).
- workspaceSwitchUrl adds /apps_raw/edit|get to EDIT_PAGES so switching
  workspace from a raw-app editor/viewer goes home like other item pages.

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

* fix: scope session model, preview picker, and open-in-workspace to session

Second-layer workspace-scoping fixes from PR review: three more paths
resolved the global navigation workspace instead of the session's
effective workspace.

- SessionWrapper loads copilot config (models/providers) for the session's
  acting workspace, so getCurrentModel/modelProvider match the workspace
  the chat writes to, not the nav workspace.
- PreviewRouterPicker takes a workspaceId prop; the sessions page passes the
  session's effective workspace so the breadcrumb/+ picker lists fork items
  and its drafts, not the nav workspace's.
- 'Open in workspace' appends ?workspace= via the new withWorkspaceParam so
  the full-page link opens the active preview under the session workspace.

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

* fix(sessions): only the active session loads global copilot config

Follow-up to the session-workspace copilot fix: SessionWrapper's
loadCopilot effect ran in every warm/hidden wrapper, and since
copilotInfo/copilotSessionModel are global, a background session in a
different workspace could finish loading after the active one and leave
the active chat on the wrong provider/model. Gate the load on
currentSessionId so only the active session writes the shared config.

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

* fix(sessions): guard copilot load race + scope app handoff to workspace

Two more session-vs-navigation workspace fixes from PR review:

- loadCopilot now applies only the most recent call's result via a
  monotonic token, so a stale async load from a just-switched-away session
  can't clobber the active session's global model/provider config.
- navigateEditorTo carries the session workspace on the low-code app
  handoff (goto /apps/edit) so the app opens in the fork the session acts
  on, not the navigation workspace.

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

* fix(sessions): scope live-editor breadcrumb picker to session workspace

The session preview's live script/flow/raw-app editors mount with a
session workspaceId, but their EditorHeader breadcrumb picker
(WorkspaceItemDrillPicker) still loaded items and drafts from the global
navigation workspace. Thread an optional workspaceId prop from each
builder's autosaveWorkspace through EditorHeader -> BreadcrumbSegment ->
WorkspaceItemDrillPicker; it falls back to $workspaceStore, so non-session
editors are unchanged.

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

* docs(sessions): fix stale setSessionTabs transient-persistence comment

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

* fix(sessions): scope live-editor deploy/save/triggers to session workspace

The session preview's live script/flow/raw-app editors load and autosave
against the session's acting workspace, but their internal deploy,
save-draft, trigger-loading, fork-eligibility, worker-tags and
live-editor-draft operations read $workspaceStore directly. Since a session
deliberately leaves $workspaceStore on the navigation workspace, a
fork-scoped session deployed/saved to the wrong workspace (verified: deploy
POSTed to the nav workspace and 400'd).

Introduce an opWorkspace derived (autosaveWorkspace ?? $workspaceStore) in
each builder and route the operation reads through it. autosaveWorkspace is
only set by the session editor views, so opWorkspace equals $workspaceStore
for every non-session editor — no behavior change outside sessions. Verified
in-browser: a fork-session deploy now POSTs to the fork (201 Created) while a
normal editor still targets the navigation workspace.

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

* fix(sessions): staged pending fork chip uses default accent, not parent's color

A staged pending fork's effective workspace resolves to its parent
(setSessionPendingFork sets pending_workspace_id = parent_workspace_id),
so WorkspaceScopeTrigger read the parent workspace's color and painted the
'Acting on' chip in the parent's hue (e.g. yellow) instead of the neutral
fork accent. A real fork shows its own color; a not-yet-created one has
none, so fall back to the default fork accent unless the creation form
passes an explicit color preview.

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

* fix(sessions): scope raw-app deploy/save/version to session workspace

The raw-app create/update/version/diff/save operations live in
RawAppEditorHeader (not RawAppEditor), and still read $workspaceStore — so
a fork-scoped session's raw-app deploy targeted the navigation workspace,
the same class of bug already fixed for scripts and flows. Route those
operation reads through opWorkspace (autosaveWorkspace ?? $workspaceStore);
the inSessionPane-guarded draft-cleanup blocks are intentionally
non-session and keep $workspaceStore. Verified in-browser: a fork-session
raw-app deploy POSTs update_raw to the session fork (200).

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

* fix(sessions): key live-editor load cache on workspace, not just path

The script/flow/raw-app loaders returned early when loadedPath matched the
requested path, ignoring the workspace. Retargeting a session to the same
item path in a different fork kept the old workspace's loaded content while
the editor props switched to the new workspace — so save/deploy/autosave
could write stale old-workspace content into the new fork. Add
loadedWorkspace to the load slot and include it in the early-return guard so
a same-path/different-workspace retarget reloads. Verified in-browser: the
script re-fetches from the new fork on an acting-workspace switch.

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

* fix(sessions): drop stale content when a live editor retargets to a new workspace

Follow-up to keying the load cache on workspace: the loaders reloaded on a
workspace retarget but did not clear loadedPath during the fetch, so
SessionEditorTarget's loadedPath-keyed ready/notFound/stale gates still
treated the editor as ready on the old workspace's content — the outbound
draft sync (now wired to the new workspace) could write stale content into
the new fork, and a 404 kept rendering the old editor. Clear loadedPath on a
workspace change too (like a force reload), so the loading/not-found gates
and the draft-sync ready check resolve correctly. Same-workspace path swaps
are unaffected (loadedWorkspace still matches, so the old editor stays
visible during the swap).

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

* fix(sessions): await committed-workspace copilot config before a session send

getCurrentModel() reads the global copilotInfo when the request builds, but
SessionWrapper's loadCopilot for the active session is fire-and-forget — so a
send right after switching to a session in another workspace could pick the
previous workspace's provider/model while the proxy clients and tools target
the new workspace. Track the workspace copilotInfo reflects (copilotWorkspace)
and, in the session beforeSend hook (awaited before the request builds), load
the committed workspace's config when it doesn't already match. Verified
in-browser: sending a session committed to a workspace whose copilot config
wasn't yet loaded fires get_copilot_info for it just before the LLM proxy call.

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

* fix(sessions): navigate to a fresh session on reset; dedupe preview page tabs

Two review findings:

- resetToNewSession (deleting/archiving the open session) and the sidebar
  delete of the active last session created/selected a fresh session but left
  the URL on the old session_name. The page derives the visible session from
  that query, not currentSessionId, so it showed the deleted session's
  not-found state (or stayed on the archived one). Navigate to the fresh
  session, matching how activate()/enterSessionMode already switch sessions.

- Preview page-tab dedupe: the iframe reports its location with the injected
  nomenubar/workspace params, but tabs dedupe the observed loc against the
  workspace-less canonical url, so reopening a page spawned a duplicate tab.
  Canonicalize the observed loc in observeLocation (dropping both params);
  covered by a new sessionPreviewTabs test.

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

* fix(sessions): don't persist preview-iframe workspace; scope fork ducklakes to base

Two review findings:

- A sessions-preview iframe runs the logged layout, which persisted its
  ?workspace= (the session's fork) to localStorage — shared with the
  top-level app, so opening a fork preview clobbered the navigation
  workspace and reloads restored into the fork. Skip the persist when
  embedded; $workspaceStore is still set in-memory for the iframe's own API
  calls. Verified: opening a fork /runs preview leaves localStorage.workspace
  on the top-level workspace.

- ForkDucklakeSection listed ducklakes from $workspaceStore while a
  fork-of-fork is created from the selected base, so it could show the root's
  lakes and submit shared_ducklakes the base doesn't have. Add a
  sourceWorkspace prop (base ?? $workspaceStore) like ForkDatatableSection,
  and pass baseWorkspaceId at the mount.

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

* fix(sessions): keep preview iframe on session fork across reloads and open-in-workspace

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

* fix(sessions): scope worker-tag pickers to the session's effective workspace

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-07 14:27:00 +02:00
GuilhemandClaude Opus 4.8 8df613b4d2 feat(raw-apps): runtime-error overlay + AI import-React instruction (#9966)
* feat(raw-apps): render runtime-error overlay + instruct AI to import React

Render the `runtimeError` message the raw-app preview frame now posts as a
prominent overlay, so an uncaught exception that blanks the app is visible
instead of silent. Cleared on the next successful build (via a shared
`feedPreviewIframe` helper so every preview-feed path resets it).

Add an AI app-generation instruction to begin React files with
`import React from 'react'`: raw apps bundle with the classic JSX transform,
so a missing import compiles fine but throws "React is not defined" at runtime.

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

* feat(raw-apps): add the import-React rule to the shared raw-app prompt

The global AI chat and the raw-app CLI skill draw their raw-app authoring
reference from system_prompts/base/raw-app.md — a separate surface from the
app chat's inline prompt (core.ts). Add the same "always begin JSX files with
`import React`" rule there (esbuild's classic transform needs React in scope,
or JSX throws "React is not defined" at runtime) and regenerate the derived
prompt files.

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

* chore(raw-apps): bump ui_builder tarball to f8cecf9 (runtime-error overlay)

Pins the ui_builder artifact to windmill-code-ui-builder#15, which pushes
uncaught runtime errors from the preview iframe to the parent so the raw-app
editor can render them in the error overlay.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 09:32:43 +02:00
hugocasaandClaude Opus 4.8 b4b0c6a93e feat: add dev workspaces paired with a lockable prod workspace (#9793)
* feat: add dev workspaces paired with a lockable prod workspace

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

* fix: gate dev-workspace prod-lock on admin and prevent attach cycles

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

* feat: redirect locked-prod edits into the dev workspace

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

* fix: make dev-workspace settings tab available on CE (was EE-gated)

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

* feat: lock prod against forking too and funnel edits to the dev workspace

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

* feat: open dev item page on edit and tailor dev-workspace lock messages

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

* feat: prevent nested dev workspaces and hide dev option when one exists

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

* chore: drop the redundant already-has-dev hint on the fork form

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

* feat: badge dev workspaces and sort them ahead of forks in the tree/switcher

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

* feat: label dev workspaces as 'Dev workspace of X' instead of 'Fork of X'

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

* feat: label edit as 'Edit in <dev>', cover editor headers, auto-expand dev in tree

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

* feat: split prod lock into separate block-deploy and prevent-forking toggles

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

* feat: make resources/variables workspace-specific from compare page

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

* feat: steer AI-chat sessions to the dev workspace

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

* feat: refine session fork options and lock guidance for dev/prod

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

* fix: session picker reads prod's real rules, default to current ws

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

* feat: copy members into forks and clarify dev-workspace root labeling

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

* style: place the workspace id field under the fork name

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

* fix: address dev-workspace review findings and harden fork detection

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

* chore: regenerate sqlx offline cache

Restores entries dropped during the origin/main merge and adds the
dev-workspace queries (is_dev_workspace, ws_specific, has_parent).

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

* fix: address second-round dev-workspace review findings

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

* fix: address Pi and Codex review findings on dev-workspace endpoints

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

* fix: gate locked-dev git-branch fork on admin and validate ws_specific path

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

* fix: clear prod dev-lock when deleting an attached dev workspace

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

* refactor: consolidate dev-workspace migration and scope all-group join to attach

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

* fix: restore dev-workspace CHECK into consolidated migration and scope all-group join

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

* refactor: drop copy_members from the dev-workspace attach path

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

* fix: dev-workspace lifecycle/auth fixes from Codex review round

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

* feat: explicit create-in-other for workspace-specific items

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

* fix: make create-in-other strictly create-only (never overwrite target)

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

* fix: return 403 (not 401) for dev-workspace permission denials

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

* feat: allow attaching a same-family fork as a dev workspace

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

* style: emphasize the go-to-dev action in the no-direct-deploy alert

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

* fix: seed a resource's linked variables when creating it in the other workspace

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

* fix: judge workspace deploy/fork locks against the user's identity in that workspace

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

* style: clarify create-in help text in workspace-specific panel

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

* fix: admin-gate dev-workspace creation and harden lock/seed edges

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

* fix: preserve a staged fork's source on picker create-mode re-entry

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

* fix: clear dev flag on archive and check dev existence server-side

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

* fix: make create-in-other atomically create-only via direct create

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

* fix: create-only resource insert, ws-specific list scopes, archive lock guard

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

* fix: reserve the dev_workspace_lock protection-rule name from the public API

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

* docs: reattach create_protection_rule doc comment to its function

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

* refactor: make dev-archive pairing teardown atomic with the archive

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

* fix: follow deploy_to on root rename; show dev pairing to non-member prod admins

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

* fix: copy creator metadata on fork; invalidate fork routing cache on rename

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

* fix: accept g/ paths in set_ws_specific; gate copy_members to dev workspaces

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:01:07 +00:00
Ruben FiszelandClaude Opus 4.8 da45e699c8 feat(apps): add labels input to app editor deploy drawer (#9828)
* feat(apps): add labels input to app editor deploy drawer

The labels feature (c4c9ef5fd) wired LabelsInput into the script, flow,
schedule, resource and variable editors but left the app editor out: it
had no labels state and createApp/updateApp never sent labels, so apps
could not be labeled from the UI despite full backend support.

Thread the deployed app's labels from the edit page through AppEditor
into AppEditorHeader, render LabelsInput in AppEditorHeaderDeploy after
the summary field (matching ScriptBuilder/FlowSettings), and include
labels in the create/update request bodies, the savedApp snapshot, and
the diff/deploy comparison values. The raw-app editor shares the deploy
drawer, so it is wired symmetrically (createAppRaw/updateAppRaw + the
raw page loader) to avoid leaking a non-functional input there.

Fixes WIN-2107

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

* refactor(apps): drop redundant labels cast in app edit restore path

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

* fix(apps): include labels in deploy-drawer Diff current value

The Diff button inside the deploy drawer built its current value without
labels, so the approval preview could hide label changes that would be
deployed. (Identified by cubic.)

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

* fix(apps): reset raw-app labels on new-draft seed

The raw-app edit route keeps labels as route-level state and the
?new_draft=true seed-template branch never cleared it. Since the route is
reused across raw-app navigations, opening a labeled raw app then creating
a fresh one could remount RawAppEditor with the previous app's labels and
deploy them via createAppRaw. Reset labels with the other bleed-prevention
resets at the top of the new-draft branch.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:43:09 +02:00
Ruben FiszelandClaude Opus 4.8 c479afab8e fix: redeploy older app version from deployment history (#9826)
* fix: redeploy older app version from deployment history

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

* fix: apply restored app version to low-code editor on redeploy

Redeploying an older app version from Deployment History fired the
restore callback (toast shown) but the canvas kept displaying the
current version, and Deploy then shipped that current value.

AppEditor seeds its working state from `appDraftHandle.draft ?? app`,
preferring the per-path autosave over the freshly restored `app` prop.
The remount triggered by the restore therefore re-read the stale
pre-restore draft. `reloadDeployed` already clears the draft before
remounting for the reset-to-deployed flow; `onRestore` was missing the
same step.

Drop the autosave in `onRestore` so the remounted editor seeds from the
restored value. Raw apps are unaffected: RawAppEditor binds `files`
directly (no draft precedence), and `extractRawApp` mutates that bound
state in place.

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

* fix(raw-apps): convert savedNewAppPath event forwarding to a callback prop

`svelte-check` (CI `npm check`) failed with one error: forwarding the
`savedNewAppPath` createEventDispatcher event through the runes-mode
RawAppEditor → RawAppEditorHeader chain types as "not assignable to
never". This is the same legacy-forwarding-through-runes pattern already
removed for `restore` in this PR — `on:savedNewAppPath` would likewise be
dropped at runtime, breaking navigation to the new path after a deploy
that renames the app.

Replace the `on:savedNewAppPath` forwarding with an `onSavedNewAppPath`
callback prop threaded page → RawAppEditor → RawAppEditorHeader, matching
`onRestore`. The header now invokes the callback instead of dispatching,
and its now-unused createEventDispatcher is removed.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:25:59 +02:00
hugocasaandClaude Opus 4.8 d865518934 feat: detect and guard against deploying stale drafts (#9768)
* feat: detect and guard against deploying stale drafts

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

* feat: extend stale-draft warning to low-code app drafts

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

* refactor: discard stale draft on rebase instead of resetting to latest

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

* feat: animate AI chat thinking block open/close like tool calls

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

* feat: detect stale flow/app drafts by pinned version at load and deploy

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

* fix: reset version-staleness state on new drafts and after app deploy

Addresses review: new-draft route reuse left stale version/draftBaseVersion (false stale-draft modal on a fresh flow/app); app deploy left parent_version pinned to the superseded base (false 'not latest' on a follow-up deploy).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:12:12 +00:00
Ruben FiszelandClaude Opus 4.8 f99781ca5f fix: persist on-behalf-of user across app deploy paths (#9773)
* fix(frontend): persist on-behalf-of user when redeploying raw apps

The raw-app deploy drawer reused AppEditorHeaderDeploy but never wired up
the `preserveOnBehalfOf` bindable nor forwarded `preserve_on_behalf_of` in
the createAppRaw/updateAppRaw request bodies. Without that flag, the shared
backend handler (create_app_internal/update_app_internal) resets the policy's
on_behalf_of to the deploying user on every deploy. So a publisher who set
"App executed on behalf of <other user>" would silently lose it on the next
deploy, unlike every other setting on the deploy page.

Mirror the classic (low-code) app header: declare `preserveOnBehalfOf`, bind
it to the deploy component, and send `preserve_on_behalf_of` on both create
and update.

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

* fix(frontend): preserve on-behalf-of in the draft-deploy path

The draft-deploy path (deployDraft → AppService.createApp/updateApp for visual
apps, deployRawAppDraft → createAppRaw/updateAppRaw for raw apps) carries the
deployed app's policy forward but never sent preserve_on_behalf_of. So
deploying a draft via the "Review & deploy drafts" UI silently reset the
policy's on_behalf_of to the deploying user — the same backend reset behind the
deploy-drawer bug, on a surface that has no on-behalf-of selector to re-set it.

Send preserve_on_behalf_of whenever the carried policy has an on_behalf_of, for
both app types. The backend still gates actual preservation on
can_preserve_on_behalf_of, so a non-deployer cannot escalate.

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

* fix(frontend): preserve on-behalf-of in the AI-chat raw-app deploy

The global AI-chat deploy path (`deploy_workspace_item` → createAppRaw/
updateAppRaw in copilot/chat/global/core.ts) carried the recomputed policy
forward but omitted preserve_on_behalf_of, so deploying a raw app via chat
reset the policy's on_behalf_of to the deploying user — the last of the
deploy surfaces with this gap. Send the flag when the policy has an
on_behalf_of, mirroring the editor and draft-deploy paths; the backend still
gates preservation on can_preserve_on_behalf_of.

Add a regression test asserting the flag is forwarded when the deployed
policy carries an on_behalf_of.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:10:21 +00:00
GuilhemandClaude Opus 4.8 a116715c41 feat(frontend): restore raw app 'open preview in separate window' (#9765)
* feat(frontend): restore raw app 'open preview in separate window'

Re-adds the detached preview window dropped when preview hosting moved to
the host (ui-builder f52d8e5b). Live-synced preview + dark mode, and wires
the runnable bridge to the detached window so backend calls resolve there.

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

* fix(frontend): repaint detached raw app preview after refresh

The detached preview tab is a blank app-preview.html shell fed by the
editor over postMessage. A one-shot opener load listener can't survive the
tab refreshing itself, so a manual reload left it blank. It now posts
'appPreviewReady' on every (re)load and the editor re-sends the build; the
orphaned window is also closed when the editor unmounts.

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

* fix(frontend): keep load-based feed for detached preview initial open

Relying solely on the appPreviewReady handshake left the detached window
blank on first open against app-preview.html artifacts that predate the
handshake (the pinned UI Builder tarball). Restore the one-shot load feed so
initial open works regardless of the served shell; the handshake still
covers manual refresh.

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

* fix(frontend): re-sync dark mode when refocusing detached preview

The focus-reuse path replayed the build but not the theme, so re-opening an
existing detached window after a dark-mode toggle kept the stale theme until
the next build. Extract a feedExternalPreview() helper (theme + build) used by
the open, focus-reuse, load and handshake paths.

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

* chore(frontend): bump ui_builder artifact to 062d11c (preview handshake)

Pins the UI Builder artifact built from windmill-code-ui-builder#14, which
adds the appPreviewReady handshake, detached-preview favicon and title.
Activates refresh-repaint + favicon for the detached raw app preview.

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

* chore(frontend): serve ui_builder static bundle in dev, proxy :4000 only as fallback

The postinstall downloads the pinned UI Builder artifact into static/ui_builder,
which SvelteKit already serves at /ui_builder. Skip the :4000 proxy when that
bundle is present so dev uses it directly (matching prod / the backend's
static-vs-:4000 fallback); no separate UI Builder dev server needed. Delete
static/ui_builder to develop the builder against :4000.

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

* fix(frontend): harden detached preview origin + scope window name

Addresses cubic review on #9765:
- P1: only honor appPreviewReady from a same-origin sender and post the build
  with targetOrigin=location.origin, so user app code that navigates the
  detached window cross-origin can't trigger/receive a build (app source).
- P2: scope the detached window name per app path so two open editors don't
  collide over one OS-level preview window.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:39:16 +00:00
GuilhemandClaude Opus 4.8 e98df38ac4 feat(apps): show raw-app fork diffs as per-file tree items (#9491)
* feat(apps): show raw-app fork diffs as per-file tree items

Raw-app diffs previously rendered as one big YAML diff of the whole
serialized app. This explodes a raw app into separate, independently
collapsible diff items — one per file, one per runnable, and an
app.yaml metadata item — that flow through the existing fork-diff
list, sidebar tree, search and count via composite paths
(<appPath>/<file>). Runnables render as script/flow rows (code shown
in a Content tab), and files get extension-specific icons reused from
the raw-app editor.

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

* chore: remove raw-app tree-diff plan doc from the branch

The implementation plan was an authoring aid, not product documentation; drop it so it doesn't ship in the PR.

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

* feat: present raw app as an app-headed folder in the diff tree

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

* fix: narrow RawAppFileItem in diff viewer branch (fixes svelte-check)

DiffRow.kind is a plain string so the kind check didn't narrow the union; assert the synthetic item. Also size-guard on the larger side's line count instead of the doubled total, and document normalizeRawApp's per-field value-wrapper precedence.

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

* style: single-line, lighter diff-tree rows for all item kinds

Add a singleLine mode to WorkspaceItemRow (summary ?? path on one line; DRY'd via a shared body snippet) and use it for every diff-tree leaf, so scripts/flows/triggers/resources/etc. match the raw-app header. Bump rows to py-1.5, force font-normal, and split colours: items in text-primary, folders in text-secondary.

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

* refactor: extract pure diffTree model from WorkspaceDiffDrawer

Move tree construction + keyboard-nav traversal + the folder-keying convention out of the 775-line component into a pure, generic, tested module (buildDiffTree → root/order/parentKeyOf/firstChildKeyOf). Parent and first-child come from a child→parent map built during construction, not from re-splitting a path at the call site, so a node's tree position and its nav parent can't drift — the class of bug behind the ArrowLeft regression. Deletes the forkDiffNav half-seam (its bug lived in the untested caller). 12 new unit tests cover order/parent/first-child incl. the storage-key-vs-friendly-path case.

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

* fix(apps): keep raw-app metadata flag + dedup runnables across path collisions

Addresses two P2 review nits (Codex/claude): (1) rawAppDiffToItems marked metadata by matching path==='app.yaml', so when a real file is named app.yaml the reserved app.yaml~2 metadata item lost its flag/full-YAML toggle — now parseRawAppDiff tags the entry with isMetadata and the items read the flag; (2) runnable composite leaves weren't deduped against real files, so a real file at runnables/<name> could produce a duplicate leaf — now reserved (slash-normalized) like parseRawAppDiff. +2 tests.

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

* fix(apps): dedup /app.yaml metadata collision + disambiguate synthetic row keys

Two follow-up P2s from Pi/Codex re-review of the prior fix: (1) parseRawAppDiff's collision set used raw file keys, so a real file /app.yaml (leading slash, which joinAppPath strips) still collided with the synthetic app.yaml leaf — now slash-normalized via a shared stripLeadingSlash, +test. (2) synthetic raw-app items (runnables rendered as script/flow) could share kind+path identity with a real workspace script/flow at <appPath>/runnables/<name>, causing duplicate {#each} keys and broken nav — itemKey now prefixes synthetic items (rawapp:).

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

* fix(apps): canonicalize raw-app file keys to dedup leading-slash collisions

Codex P2: a file keyed /App.tsx on one side and App.tsx on the other became two entries that joinAppPath collapsed to one composite path → duplicate row key. asFileMap now strips the leading slash so both sides resolve to one file. +test.

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

* perf(apps): lazy-mount per-file diff editors as they scroll into view

Exploding a raw app into N per-file rows mounted N Monaco DiffEditors at once (3 reviews flagged it). Each block's editor now mounts only when it scrolls within ~200px of the viewport (IntersectionObserver rooted on the scroll container), showing a light placeholder until then; mountedRows latches so it never unmounts on scroll-away. Verified: ~6 of 13 mount initially, the rest on scroll.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 09:23:14 +02:00
2879cbb65a feat(apps): opt-in sandbox isolation for published & raw apps (alpha) (#9420)
* feat(apps): sandbox published & raw apps with a scoped embed token

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

* chore: point ee-repo-ref at embed-token EE commit

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

* feat(apps): allow top-navigation from the sandboxed app iframe

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

* feat(apps): share app localStorage across apps via the embedder

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

* feat(apps): publisher disable-sandbox option with per-version viewer consent

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

* chore(sqlx): cache for disable-sandbox queries

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

* chore: bump ee-repo-ref to disable-sandbox EE commit

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

* fix(apps): always sandbox the served raw-app wrapper + viewer fixes

The raw-app wrapper served by get_raw_app_data now always carries
`CSP: sandbox`. The publisher "disable sandbox isolation" opt-out is applied
entirely on the viewer side, which (after per-version consent) builds its own
same-origin blob wrapper — so the backend-served document stays isolated
regardless of how it is reached, never via a relaxed real-origin URL.

Also:
- CORS on the global /apps_u mount so the opaque viewer can load custom-path
  public apps cross-origin.
- Reject runnable-bridge messages unconditionally until the iframe is bound.
- Relay the viewer's in-app hash up to the embedder address bar so deep links
  stay shareable (hash only; embedder keeps its own pathname).

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

* feat(apps): render public raw apps single-iframe (drop embed token)

Public raw apps now render directly on the real origin with a single
opaque bundle iframe and the page credential, instead of the opaque
viewer + scoped-token indirection. The author bundle stays isolated in
its own opaque iframe (CSP-sandboxed); low-code apps, whose code runs in
the viewer frame, keep the opaque viewer + scoped token.

embed_token now reports raw_app and skips minting a token for raw apps;
the access check still gates visibility.

Also set disable_sandbox: None in the remaining Policy constructors so
the full feature build (all_sqlx_features, enterprise, license) compiles.

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

* chore: bump ee-repo-ref to single-iframe raw-app EE commit

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

* feat(apps): grandfather existing apps as legacy-unsandboxed + authed-only consent

Existing apps are stamped by migration as `legacy_unsandboxed` so they keep
running same-origin on upgrade — no breakage and no consent prompt. New apps are
sandboxed by default; re-deploying an app clears the flag.

The publisher `disable_sandbox` consent prompt is now shown only to authenticated
viewers — an anonymous viewer has no session to expose, so the prompt was
meaningless friction.

embed_token reports `legacy_unsandboxed` and `authed`.

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

* chore: bump ee-repo-ref to legacy-unsandboxed EE commit

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

* feat(apps): deploy-time migration prompt for legacy-unsandboxed apps

On the first re-deploy of a grandfathered (legacy-unsandboxed) app, the
publisher must explicitly choose: enable sandbox isolation (the flag is
cleared → the app becomes sandboxed) or keep running without isolation
(→ disable_sandbox, with per-version viewer consent). updatePolicy() no
longer carries the legacy flag through a deploy, so the choice is what
sticks.

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

* fix(apps): disable the sandbox-isolation toggle until the app is deployed

The Deploy-drawer "Disable sandbox isolation" toggle called setPublishState()
— which updates the app by path — even before the app was first deployed, when
the path is empty, throwing an error. Guard it with disabled={!savedApp},
matching the adjacent visibility toggle.

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

* feat(apps): sandbox the in-workspace low-code app viewer in an opaque iframe

Extend the opaque-origin iframe isolation to the logged-in /apps/get viewer.
/apps/get becomes an embedder that keeps the workspace chrome + Edit button and
renders the app inside a cookieless, chrome-less /app_embed viewer route, handed
a scoped embed token minted from the member's session. The app frame runs in an
opaque origin (no allow-same-origin), so it cannot reach the member's session
cookie or window.parent.

- apps.rs: get_app_embed_token_for_path (authed, by-path, scope + RLS gated);
  mint_app_embed_token grants a path-scoped apps:read:{path} so the viewer can
  load its own app definition and no other
- lib.rs: CORS on /apps (bearer-token only, no cookies) for the opaque viewer's
  by-path reads
- new /app_embed/[workspace]/[...path] viewer route (private analog of /public)
- PublicAppFrame: viewerUrl prop to point the opaque iframe at the viewer route

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

* feat(apps): unify in-workspace app viewers on the shared sandboxed path

Route every in-workspace app display (low-code and raw) through the same
PublicAppFrame -> PublicApp machinery as the public viewer, so the sandbox /
legacy-unsandboxed / disable-sandbox-consent behavior is identical on every page.

- new InWorkspaceAppViewer renders both app types via PublicAppFrame; /apps/get
  and /apps_raw/get become thin wrappers over it
- /apps_raw/get previously rendered RawAppPreview directly (always isolated, with
  no legacy-grandfathering or consent handling); now consistent with the rest
- retire the legacy same-origin raw viewer /apps/get_raw/[version] and re-point the
  apps-list row to /apps_raw/get; remove the dead /apps_raw/[ws]/[version] route
- load the raw bundle secret in the shared viewer (getAppByPath doesn't return it)

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

* fix(apps): address PR review feedback (scope + policy hardening, nits)

- require handler-level apps:read on list_apps / list_search_apps so a scoped
  embed token cannot read app definitions through the list endpoints. The route
  layer treats apps:run as satisfying read; the handler check (which does not)
  closes the gap.
- treat legacy_unsandboxed as backend-owned: strip any client-provided value in
  create/update so it can only be set by the grandfather migration, not the API.
- document mint_app_embed_token's caller-verifies-access contract.
- use Button's declared onClick prop for the consent action (was onclick, which
  fell into the rest-spread and bypassed the component's click handling).
- test: lock that the embed scopes cannot satisfy domain-level apps:read.

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

* docs(apps): document embed-token endpoints in openapi + fix doc nit

Second-round review nits:
- add the three app embed-token endpoints (apps/embed_token/p/{path},
  apps_u/embed_token/{secret}, and the EE apps_u/embed_token_by_custom_path) plus
  the EmbedTokenResponse schema to openapi.yaml; note .html on get_data
- mint_app_embed_token doc: "Both" -> "All" (it lists three call sites)

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

* feat(apps): bound embed-token scopes to the caller's own

The embed-token mint now enforces ensure_scopes_within_caller, so the
minted scope set is always within the calling credential's own scopes
(a no-op for regular unscoped sessions). Adds a unit test locking the
boundary and documents the contract on mint_app_embed_token.

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

* fix(apps): raw-app ctx in external embeds + page credential in direct render

- RawAppPreview: engage the storage relay only in opaque frames (probe Web
  Storage instead of just window.parent), so a public raw app embedded in an
  external iframe hydrates ctx/storage directly; add a relay-timeout fallback
  so an unresponsive parent can never stall the ctx handshake.
- PublicAppFrame: in direct render, expose the page's own bearer credential
  through the AuthToken context (JWT public URLs), matching the previous
  route behavior; opaque-viewer mode keeps the embed token.

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

* feat(apps): sandbox isolation UI polish + COI embed support for raw apps

- Deploy drawer: move the sandbox toggle out of "Public URL" into its own
  "Sandbox isolation" section (the setting applies to every viewing surface,
  not just the public URL), with positive phrasing, visible helper text, and
  state-aware alerts (warning when disabled, info for pre-isolation apps).
  Toggling it now toasts its own message instead of the login-mode one.
- Extract the deploy-time migration prompt into a shared
  LegacySandboxMigrationModal built on the common Modal component, and wire
  it into the raw app editor header too (it previously had no prompt, so
  re-deploying a pre-isolation raw app silently changed behavior).
  updateRawAppPolicy now also drops the backend-owned legacy flag, matching
  the low-code updatePolicy.
- Viewer consent prompt: use the common ConfirmationModal and show the app
  path (new appPath prop) instead of the route pathname, falling back to
  "this app" when the path isn't known yet.
- COI embeds: propagate the wm_coep opt-in to the raw-app wrapper document
  and have the backend assert COEP require-corp on it when the flag is
  present — required for the bundle iframe to load when the public app page
  is embedded inside a cross-origin-isolated page. Previously this only
  worked in dev because the Vite proxy injects the header; the production
  response lacked it.

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

* feat(apps): app navigation parity across sandboxed and direct viewers

- Navbar component: same-app items relay query + hash to the embedder page
  (which mirrors them onto the root URL, keeping its own pathname and
  transport params), app items navigate the top page through a validated
  wm_embed_navigate relay instead of the cookieless viewer iframe, and
  external items keep opening a new tab. Selected-item detection now
  recognizes the /app_embed viewer route and ignores transport params.
- Frontend-script `goto` and button `onSuccess: gotoUrl`: same-window
  navigation goes through a shared appNavigateSameWindow helper that relays
  to the embedder inside the opaque viewer (same-origin paths SPA-navigate,
  http(s) URLs do a full load, other schemes rejected) and keeps plain
  window.location everywhere else.
- /apps/get and /apps_raw/get: key the viewer by workspace/path so in-route
  navigation fully remounts it — previously the URL changed but the app (and
  in sandbox mode its path-scoped token) did not follow.
- wm_embed/wm_embedder_origin added to the reserved query params so they no
  longer leak into the app's ctx.query.
- Raw apps: drop the sandbox attribute entirely for the unsandboxed
  (grandfathered/consented) blob path, matching the pre-isolation viewer
  exactly — the attribute added no isolation there and sandboxed popups
  (e.g. OAuth flows).

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

* fix(apps): preserve grandfathered policy across updates + in-workspace viewer parity

Round of compatibility hardening so pre-existing apps behave exactly as
before on every surface:

- `legacy_unsandboxed` is now preserved across app updates unless the payload
  explicitly clears it (`false`, sent by the editor's migration prompt and the
  sandbox toggle). Unrelated update paths — CLI / git-sync redeploys,
  publish-mode toggles, cross-workspace promotion — no longer silently drop
  the grandfathering. Clients still can never SET the flag.
- The embed-token endpoints (secret, path, EE custom-path) read only the
  sandbox-decision policy fields, leniently, and no longer mint a token for
  raw / legacy / disable_sandbox renders: the token is only consumed by the
  sandboxed low-code render, and minting for the others wrote a useless token
  row per view and could fail the render for scope-restricted callers.
- In-workspace viewer parity with the pre-sandbox `/apps/get`: new
  `inWorkspace` mode on PublicApp (no "Powered by Windmill" badge / user
  overlay, no HTML-result approval gate, column flex wrapper, `hideRefreshBar`
  honored again), and the page's query/hash are forwarded into the opaque
  viewer so `ctx.query` / `ctx.hash` reach the app.
- Raw apps: `window.ctx` is always `{ctx, workspace}` again (anonymous viewers
  of pre-existing bundles rely on `ctx.workspace`), and the runnable bridge's
  job-id scoping now applies only to sandboxed renders (`gateJobIds`) — an
  unsandboxed bundle holds the same credential as the bridge, so gating there
  only broke pre-existing apps polling persisted or runnable-returned job ids.
- Document `disable_sandbox` / `legacy_unsandboxed` in the openapi Policy
  schema; add a unit test for the lenient policy read.
- bump ee-repo-ref to the matching EE commit.

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

* fix(apps): keep share-link viewer credentials out of the isolated app context

The JWT path segment of authenticated share URLs is an embedder-side
credential, consumed only to mint the scoped embed token. Two transport
channels still copied it into the isolated frame where app-authored code
runs:

- the opaque viewer iframe src defaulted to window.location.href — the
  public and custom-path routes now pass a sanitized viewerUrl (JWT segment
  stripped, query/hash preserved, captured once so the hash relay does not
  reload the iframe);
- document.referrer on the same-origin iframe navigation carried the full
  embedder URL — both app iframes now set referrerpolicy="no-referrer"
  (sandboxed renders only for the raw bundle iframe, keeping exact legacy
  parity; nothing reads the referrer).

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

* chore(frontend): drop unused import inherited from main merge

`slide` import in AssistantMessage.svelte (from #9539) turns `npm run check`
red on this branch.

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

* fix(apps): redirect the removed raw-app viewer path to the unified viewer

The old same-origin raw-app viewer route (/apps/get_raw/{version}/{path}) was
removed in favor of the sandboxed unified viewer. Re-add a thin client route at
the old path that redirects stale bookmarks to /apps_raw/get/{path}, preserving
query + hash (the pinned version is dropped — the unified viewer shows latest).

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

* fix(apps): narrow embed-token scopes and base consent on browser session

- Embed token: resource access is metadata-only (list/type/exists) via a
  `resources:run` marker — resource values (get/get_value/get_value_interpolated/
  list_search) are no longer reachable. Job reads are by-id only: an `app_embed`
  sentinel blocks the workspace-wide job enumeration/export routes (jobs/list,
  list_filtered_uuids, queue/list, completed/list, queue/export) while by-id
  result polling keeps working.
- disable_sandbox consent now gates on whether the browser holds any Windmill
  session (cookie-only whoami) rather than workspace-scoped auth, so a viewer
  logged into a different workspace is still prompted before a same-origin render.
- db-explorer: resolve the MySQL database name server-side (the metadata query
  already falls back to DATABASE()) instead of reading the resource value
  client-side; getTablesByResource derives the default db from the schema.

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

* chore(apps): trim embed-scope and consent comments

Reduce duplication — state the resource/job route exclusions and the
workspace-session-vs-cookie rationale once at their source and reference them
elsewhere; drop contrast/justification phrasing. No behavior change.

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

* feat(apps): make app sandbox isolation opt-in (alpha)

Replace the disable_sandbox + legacy_unsandboxed policy pair and the
per-version viewer consent with a single positive `sandbox` opt-in flag.
Apps are unsandboxed by default (same-origin, full session — the
pre-isolation behavior), so existing apps are unchanged and no migration
is needed. Publishers opt an app into isolation from the deploy drawer,
flagged alpha.

- Policy.sandbox: Option<bool>; EmbedTokenResponse -> {token, expiration,
  raw_app, sandbox}; mint an embed token only for sandboxed low-code apps.
- Drop the legacy-unsandboxed migration and the deploy-time migration
  prompt; remove the consent modal and the browser-session probe.
- Deploy drawer: a single "Sandbox isolation" toggle (alpha), off by
  default, shared by the low-code and raw editors.
- Bump ee-repo-ref to the companion EE commit.

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

* fix(apps): confine embed token to its intended user/folder/job routes

The embed token's broad read scopes spanned whole domains while the
matching routers are CORS-enabled for the opaque app iframe:

- users:read / folders:read were domain-wide, so the token could reach
  users/list, users/list_usage, users/username_to_email/*, folders/list,
  etc. Restrict to an app_embed-sentinel allowlist: only users/whoami and
  folders/listnames; deny the rest of those domains.
- jobs:read allowed jobs/completed/export, missed by the job denylist.
  Add it alongside jobs/queue/export.

Extend the embed-scope allow/deny test matrix to cover all of these.

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

* docs(apps): align sandbox comments with the opt-in model

The consent prompt, deploy-time migration, and legacy-unsandboxed
grandfathering were removed when sandbox isolation became an opt-in
policy flag; update the comments that still described them so they
match the two-state (default-unsandboxed / opt-in-sandboxed) reality.
Comments only, no behavior change.

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

* fix(apps): confine embed-token job reads to runs the app launched

App component jobs are stamped `created_by = the viewer`, so an embed token
reads its own runs via the launched-by-viewer fast path. The token then also
inherited the viewer's broader job access (share links, folder ACLs, admin
RLS), letting user-authored app JS reuse it to read unrelated jobs by id. Stop
embed tokens at the fast path: only jobs the viewer launched, never those
merely visible to them. Return NotFound so the untrusted app can't probe
existence.

Regression test: an embed token reads its own launched job but is denied the
foreign job (result/logs/getupdate) an admin viewer's normal token can read.

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

* fix(apps): allowlist embed-token apps/jobs routes + scope run to the app

The embed token's apps:run/jobs:read reached more than a running app needs.
Replace the job denylist with strict per-domain allowlists on the app_embed
sentinel:

- Apps: only the app's own definition (apps/get/p/<path>) and the public
  app-serving endpoints (apps_u/*). Denies workspace app inventory
  (exists, custom_path_exists, list, list_paths*).
- Jobs: only the by-id poll routes the frontend JobLoader uses. Denies job
  counts and the job_signature/resume_urls capability-minting routes (the
  by-id reads remain confined to the app's own runs).

Drop unqualified apps:run from APP_EMBED_SCOPES; mint apps:run:<path> instead
and authorize apps:run:<requested path> first in execute_component, so the
token can only run its own app's components, not another app's.

Extend the embed-scope route matrix and add a path-scoped run unit test.

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

* docs(apps): clarify the sandbox toggle vs the on-behalf-of model

The deploy-drawer sandbox copy leaned on "session" in a way that collided
with the on-behalf-of permissioning right above it. Reword it to say the
toggle governs what the app's browser-side code can reach in the viewer's
browser — distinct from who its runnables execute as — and rename the label
to "Isolate the app from the viewer's browser session".

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

* fix(apps): path-scope embed-token S3 download to its own app

The apps_u/* allowlist also admitted apps_u/download_s3_file/<path>, whose
handler authorized any authenticated caller — so an embed token minted for app
A could download app B's S3 files via B's on-behalf policy. Add the same
path-scoped guard execute_component uses: download_s3_file_from_app now checks
apps:read:<path> first, confining the token to its own app. Other path-taking
apps_u routes are already covered (writes lack apps:write; embed_token/p
path-checks; public_resource is type-constrained).

Extend the path-scoping unit test to cover apps:read (download) alongside
apps:run (execute).

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

* fix(apps): path-scope public-app-by-secret read to the embed token's app

The apps_u/* allowlist admitted apps_u/public_app/<secret>, whose handler only
checked the viewer's read access — so an embed token minted for app A could read
app B's definition by secret (confused deputy via the viewer's identity).
get_public_app_by_secret now binds a scoped caller to the resolved app with
check_scopes(apps:read:<path>), confining it to its own app; unscoped sessions
and anonymous access are unchanged.

get_raw_app_data needs no binding (pure secret capability, no caller identity).
Document the full set of app-resolving handlers the path-scoped read covers.

Bump ee-repo-ref for the companion custom-path fix.

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

* fix(apps): preserve pre-sandbox behavior for db-explorer, edit link, jwt

Three behavior-parity fixes for non-sandboxed (existing) apps that the
sandbox-isolation refactor changed incidentally:

- DB-explorer MySQL table picker: when the connection can see multiple
  non-system schemas, label the default db's tables unprefixed again. The
  resource-value read was removed globally, so identify the default db from
  the introspection script's `DATABASE() AS default_db_name` (carried on
  SQLSchema.defaultDb) instead of guessing "the single schema key". Equivalent
  to the prior resource.database match; editor-only (table picker).
- In-workspace Edit button: restore `?nodraft=true` on both /apps/get and
  /apps_raw/get, so opening the editor from the viewer loads the deployed
  version, not a draft.
- Custom-path (/a) viewer: restore the "could not authenticate user with jwt
  token" toast when a path JWT fails to resolve a user, instead of silently
  falling through.

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

* fix(apps): confine embed-token S3 downloads to the app's own keys/outputs

download_s3_file_from_app authorized any authenticated caller for any S3 key
(opt_authed.is_some() bypass). A sandboxed app's embed token carries the
viewer's identity, so app-authored JS could fetch arbitrary S3 keys readable by
the on-behalf identity, beyond the app's own declared keys or outputs.

Route app embed tokens through the same allowlist as anonymous viewers — the
app's declared allowed_s3_keys, or files produced by this app's own component
runs — instead of the authed bypass. The produced-files check is parameterized
by created_by (the embed viewer for a token, else anonymous) so a sandboxed
app's own S3 outputs still render while arbitrary keys are denied.

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

* fix(apps): let embed tokens cancel their own jobs; gate cancel to launcher

A sandboxed low-code app supersedes an in-flight component run on re-run by
canceling it, but the embed token only had jobs:read, so cancellation silently
failed and prior jobs ran to completion.

- Permit the by-id jobs_u/queue/cancel POST for app_embed tokens at the route
  layer (the only write reachable through the existing by-id allowlist).
- Gate cancel_job_api: an app_embed token may cancel ONLY jobs it launched
  (created_by == viewer). cancel_job_api had no other per-job ownership check,
  so this also confines the token instead of letting it cancel any job by id.
- /app_embed now sets workspaceStore so cancellation targets the right
  workspace instead of an empty/stale one in the cookieless iframe.

Add a shared has_app_embed_sentinel helper; cover cancel in the route matrix
and the jobs_read_auth integration test (own job cancelable, foreign denied).

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

* fix(apps): drop get_root_job_id from the embed-token job allowlist

Audit of the embed token's reachable job routes: get_root_job (jobs_u/
get_root_job_id) has no access check in its handler at all — it returns any
job's root-job id by id — and the app runtime never calls it. Remove it from
the by-id allowlist so the embed token can't probe a foreign job's flow lineage;
add a denied-route assertion.

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

* feat(apps): scope sandboxed-app localStorage per app

Sandboxed apps shared one localStorage store (one key on the real origin), so an
app could read or clobber another app's keys — and, with job ids stashed there,
reuse its embed token to read another app's job. Scope the backing store per app.

The embed-token endpoints now return the resolved app_path (EmbedTokenResponse;
not a new disclosure — the viewer already receives the path when it loads the
app). PublicAppFrame (low-code) and RawAppPreview (raw) key their backing store
by it: wm_apps_localstorage:<app_path>. Same app shares one store across its
public and in-workspace surfaces; different apps are isolated. Unsandboxed apps
are unaffected (real same-origin localStorage, as before).

Bump ee-repo-ref for the companion custom-path change.

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

* fix(apps): scope embed access checks to embed tokens + key app storage by workspace

- Apply the path-scoped read/run checks on the public-by-secret read and the
  component run path only when the caller is an app embed token, so other
  caller types keep their prior access.
- Key the sandboxed app's backing client storage by workspace + path instead
  of path alone, and return the resolved workspace from the embed-token
  endpoints so the custom-path viewer can derive it.
- Show a clear message instead of an indefinite loader when the viewer route
  is opened outside its embedder.

Bumps ee-repo-ref to 5b8476b.

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

* fix(apps): mint embed tokens only from the trusted embedder caller

An app embed token must not reach the embed-token mint endpoints; refresh
minting stays with the embedder session/JWT. Enforced at the scope route
layer and at the mint chokepoint, with a route-matrix regression test.

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

* fix(apps): support S3 upload and frontend-script S3 download in sandboxed apps

Sandboxed apps run with a scoped embed token (no cookie). Let the app's
S3 file-input upload and the frontend-script download({s3}) helper work in
that context: upload is reachable with apps:run and re-checked per-app at the
handler; the script download routes through the app-scoped apps_u endpoint
with the embed token instead of the cookie-authed job_helpers path. Default
(unsandboxed) apps are unchanged.

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

* chore: update ee-repo-ref to b0cb761bf9852974e571b2978032d310cc998517

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

Previous ee-repo-ref: e673c714a4618fdb72353a475f49c748e6016642

New ee-repo-ref: b0cb761bf9852974e571b2978032d310cc998517

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>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-06-23 10:05:00 +02:00