Phase 1 of automatic repo → Windmill sync. A monitor task (EE-licensed,
single-replica via advisory lock) git ls-remotes each auto-pull-enabled
repository ~every minute and enqueues a pull when the tracked branch moves,
reusing the {workspace_id}:git_sync concurrency key so pulls serialize with
in-flight push commits.
- windmill-store: background (no-authed) resolver get_git_repo_head_for_autopull
that resolves the repo resource (incl. $var: refs) and ls-remotes; GitHub-App
repos are skipped here and will sync via webhooks (phase 2).
- monitor.rs: poll/reconcile/persist with optimistic sha advance and failure
status; targeted jsonb update so concurrent settings edits aren't clobbered.
- edit_git_sync_repository: preserve server-owned auto_pull state on UI save.
- openapi: AutoPullSettings/AutoPullMode/AutoPullStatus + auto_pull field.
- frontend: per-repo "Automatically deploy changes from Git" toggle with last
sync status; demote the GitHub Actions link to an advanced CI option.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds AutoPullSettings/AutoPullMode/AutoPullStatus on GitRepositorySettings
(workspace_settings.git_sync JSONB), the GIT_SYNC_PULL_SCRIPT_PATH constant,
and should_pull/effective_poll_interval_s helpers with unit tests. Exports the
EE enqueue_git_pull_job primitive. Foundation for repo→Windmill auto-pull.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: refetch license key from settings when in-memory key is invalid
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: only record accepted license keys so rejected keys stay retryable
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: add get_app_runtime_logs tool to global chat
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: only handle raw app backend messages from the runner's own iframe
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add list_app_runs tool to global chat for raw app backend runs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: simplify raw app chat tool results
* nits
* nits
* chore: bump ui builder artifact
* fix: resolve pending runtime log requests on cleanup
* fix: harden raw app runtime log requests
* nits
* fix: show raw app tool results in status
* fix: cap raw app runtime log results
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): improve agent prompts/skills and workspace fork workflow
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): refuse fork --from-branch rename of a base branch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cli): auto-detect fork branch workflow, drop rt.d.ts refresh and legacy-name warning
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): reconcile raw-app generate-metadata stance (agent offers+runs)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): agent runs all CLI commands, gated on intent not on user typing them
Extends #9467's safe-vs-destructive model: the agent runs consequential commands (sync push, generate-metadata) itself too, gated on explicit user intent rather than handed to the user to type. The explicit-intent rule is the safeguard; an approval prompt is treated as a possible backstop, not assumed (auto-approve/headless runs have none).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Revert "docs(skills): agent runs all CLI commands, gated on intent not on user typing them"
Reverts 9225e1759b. That commit over-reached: #9467 already established the safe-vs-destructive split, and the targeted item-6 fix already removed the passive "tell the user they can run <safe next step>" phrasing. The blanket "agent runs everything" principle pushed deploys to be more eager and carried a wrong "permission layer prompts for approval" claim (untrue in auto-approve/headless mode). Keep deploys conservative.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): default fork workspace name/id to the current branch when renaming it
When 'wmill workspace fork' converts the current working branch into the fork branch, default the fork's name and id to that branch (sanitized to a slug, since branch names can contain '/'). Interactive: the prompt is pre-filled (enter to accept); non-interactive (--yes): used automatically. Adds a unit test for the slug derivation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): address fork review — guard fork-branch rename, cap+validate fork id
Two P2s from review:
- --from-branch refused when the current branch is already a fork branch (would detach the existing fork by renaming its branch).
- fork id slug capped to 42 chars (backend max 50 incl. wm-fork- prefix); auto-derived id is slugged; full id validated client-side before existsWorkspace/datatable cloning so an invalid id fails fast instead of leaving cloned Postgres databases behind.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): stop live activity flickering when user has multiple tabs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): use hasOwnProperty instead of Object.hasOwn for ts lib compat
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): show AI sessions in narrow-screen burger menu
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): show burger menu on sessions page in narrow setup
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: add reasoning effort control and thinking display to AI chat
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: strip legacy /thinking suffix from configured model slots
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: refine reasoning effort UX and drop dynamic-capability scaffolding
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: show textless reasoning on the typing indicator
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(worker): #ssh directive to run a bash script on a remote SSH host
Add a first-class `#ssh <resource_path>` bash directive that reroutes a
normal bash script to run on a remote host reached over SSH (a
jump/utility node) instead of on the worker, with full parity: typed
positional args in, structured result out, live streamed logs,
cancellation, and remote exit-code propagation.
It mirrors the existing `# sandbox <image>` precedent: the directive is
parsed in handle_bash_job and reroutes to a specialized handler that
reuses handle_child for all execution plumbing.
- windmill-common: BashAnnotations::ssh_target() parser (+ unit test)
and the ssh_execution_enabled instance setting (off by default)
- windmill-worker: reroute hook in bash_executor + ssh_executor_oss
shim. OSS returns a clear "enterprise feature" error; the real
handler lives in ssh_executor_ee.rs (private feature) and is gated by
a valid enterprise license + the instance setting.
- examples/usecase/ssh-execution-wrapper: the ssh_target resource type,
a userland wrapper (no-license fallback), and a README documenting
both paths and the trade-offs vs agent workers.
EE companion: windmill-labs/windmill-ee-private (ee-repo-ref.txt bumped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(worker): ssh host-key opt-in, 0600 key write, instance setting UI
* chore: update ee-repo-ref
* feat(worker): #ssh $arg form to take the ssh target from a job argument
* fix(worker): #ssh token must look like a target; $arg restricted to path strings
* fix(worker): tighten #ssh parser to exact directive; add -- ssh destination guard
* chore: update ee-repo-ref to d45b9a6cbe40f7fe5d322c850c50f64a6980e4f0
This commit updates the EE repository reference after PR #609 was merged in windmill-ee-private.
Previous ee-repo-ref: 2804f1aa8e74b3a7733aeb6f5044d5085193872a
New ee-repo-ref: d45b9a6cbe40f7fe5d322c850c50f64a6980e4f0
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>
* feat(ai-chat): collapse big pastes, cap input height, escape HTML
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(ai-chat): concentrate paste expand/render in a ChatDraft module
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(ai-chat): step caret over paste chips as one unit
Arrow-Left/Right now jump edge-to-edge across a collapsed-paste chip
instead of crawling through the invisible token characters, and snap the
caret out if it lands inside a token. Shift extends the selection across
the whole chip; word/line jumps (alt/cmd/ctrl) are left to the browser.
Mirrors the existing atomic-deletion behavior so traversal and deletion
both treat the chip as a single object.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): atomic chip deletion on overlapping selections + plural label
A selection that partially overlapped a paste token previously hit the
collapsed-caret early-return, so the browser deleted a partial token and
left an orphaned zero-width run plus a dangling pastes registry entry.
handlePasteDeletion now widens the deletion range to cover each overlapped
token whole and drops all affected pastes entries (shared removePasteRange
helper). Strict overlap, so abutting a chip edge doesn't pull it in.
Also route line-count pluralization through a shared lineCount() helper so
a 1-line paste reads "1 line" in the conversation bubble too, not "1 lines".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): full chip atomicity via beforeinput + expand on copy/cut
Addresses review nits on PR #9487:
- Chip atomicity is no longer limited to Backspace/Delete keydown. A new
beforeinput handler takes over any selection-spanning edit that overlaps
a paste chip — typing over a selection, paste, drag-and-drop, and
Backspace/Delete over a selection — and applies it to the whole token(s)
via a shared replacePasteRange, so a partial edit can no longer leave an
orphaned zero-width run or a dangling pastes registry entry. handlePaste
is likewise widened so a large paste onto a chip replaces it whole. The
keydown handler now only covers the collapsed-caret-at-boundary case
beforeinput can't see.
- Copy/cut of a selection containing a chip now puts the expanded content
on the clipboard instead of the chip label + its zero-width run; cut also
removes the chip whole. This also removes the duplicate-token re-paste
vector (the clipboard never carries a raw token anymore).
- Rename lineCount -> lineCountLabel: it returns a formatted string, not a
count, so the name shouldn't read like an accessor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): address cubic + claude review findings on the paste feature
cubic-dev-ai findings:
- autosize: clear inline overflow-y on the uncapped path so a capped->
uncapped toggle can't leave a stale `auto`/`hidden`.
- pasteTokens: new countLines() ignores a single trailing newline, so a
10-line paste with a trailing \n no longer counts as 11 (off-by-one in
shouldCollapsePaste and the chip's reported line count).
- ContextTextarea: the @-mention picker anchor now subtracts the textarea's
own scrollTop/Left and is recomputed on internal scroll, so it stays
pinned once the input is capped at 40vh and scrolls.
claude re-review findings:
- ContextTextarea: drag-and-drop of a selection containing a chip now puts
the expanded content on the drag payload via ondragstart (mirroring
copy/cut), so a dragged chip no longer orphans its token and loses the
pasted content (or leaks the label to an external target).
- ContextTextarea: handlePasteBeforeInput now guards on e.cancelable and a
HANDLED_INPUT_TYPES whitelist, so historyUndo/Redo (Ctrl+Z) and
non-cancelable insertCompositionText (IME) are left to the browser
instead of being reinterpreted as a delete / rewritten mid-composition.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: clear conflict error + force delete when reusing a fork workspace id
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: guard fork force-delete against double submit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(ai-chat): quick access to AI prompt settings from chat
Add a cog next to the AI model selector in the chat that opens a dropdown
to edit the current chat mode's user (localStorage) and workspace (DB
ai_config) AI prompts, reusing the shared AIPromptsModal. Workspace prompt
editing is admin-only; non-admins get a read-only view with an info Alert.
The dropdown and modal footer include an admin-only deep-link to the full
AI settings page, opening in a new tab.
Workspace save re-applies the saved custom_prompts onto the store after the
ai_config round-trip, since effective_ai_config falls back to the instance
config (and would drop them) when the workspace has no providers of its own.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): snapshot mode when opening prompt settings modal
Prompt edits were keyed off the live chat mode (a $derived of
aiChatManager.mode). If the chat mode changed while the modal was open,
save/reset/hasChanges and the modal's target mode would follow the new
mode and write to the wrong key. Capture the mode into an activeMode
snapshot at open time and use it throughout the modal lifecycle.
Identified by cubic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): base-prefix AI settings links and reset hasChanges after save
- Prefix the AI-settings deep-links (dropdown item + modal footer) with
{base} so they resolve under a deployment base path instead of 404ing.
- After a successful save, sync customPrompts to the trimmed value so
hasChanges flips back to false (Save/Reset disable, Reset no longer
snaps the textarea).
Identified by Claude review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): gate workspace prompt editing on workspace having own providers
When a workspace has no AI providers of its own (it uses instance defaults),
the backend never makes workspace custom_prompts effective — get_copilot_info
returns the instance config verbatim. A workspace prompt saved in that state
would be dead config that silently disappears on reload, and the earlier
re-apply hack also transiently replaced instance prompts in copilotInfo.
Mirror the settings page (AISettings.svelte): detect the workspace's own
providers in the same getSettings fetch used to seed the prompt, and when there
are none, surface the workspace prompt read-only with an explanatory Alert
(new readOnlyReason prop on AIPromptsModal) instead of an editable field.
Since editing is now gated to workspaces with their own providers,
effective_ai_config equals the saved config, so the re-apply is reverted to a
plain setCopilotInfo(effective).
Identified by Claude review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): await async save before closing prompts modal
AIPromptsModal.handleSave called onSave() without awaiting, then closed the
modal immediately — so the workspace save round-trip was still in flight when
the modal dismissed, with no loading state and any error toast landing after
the modal was gone.
Make handleSave await onSave (now typed to allow a Promise), show a loading
state on Save while in flight, and keep the modal open if the save throws.
AIChatSettingsMenu.save() re-throws on failure so the modal stays open. The
synchronous user-prompt path and existing callers are unaffected.
Identified by Pi review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): clarify trigger filters evaluate decoded JSON, not base64
The filter description in TriggerFilters didn't explain that filters are
evaluated on the original decoded JSON payload, while a base64-encoding
trigger (Kafka) still delivers the payload to the runnable as a base64
string. Users saw base64 in their scripts and assumed filters couldn't
reference JSON keys.
Adds a `payloadBase64Encoded` prop (set by the Kafka editor) that appends
a sentence clarifying the runnable receives base64 while filter keys
reference the original JSON structure. WebSocket triggers deliver the raw
message string, so the base64 caveat is intentionally not shown there.
Fixes WIN-2029
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): reword trigger filter help to "parsed as JSON", not "decoded"
Nothing is base64-decoded during filtering: WebSocket parses the text
frame directly, Kafka converts the message bytes to UTF-8 then parses.
The base64 form only exists on the delivery path to the runnable (Kafka,
V2). Reword to "filters match against the message parsed as JSON" and, for
base64 triggers, clarify filters run on the message before encoding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): correct filter-key example to match backend semantics
Review follow-up: the "data.status" example implied dot-path filter keys,
but the backend (SupersetVisitor in filter.rs) matches keys literally
against top-level JSON fields — "data.status" would never match a nested
object. Reword to state keys match top-level fields and nested matching
is done via an object value (key data, value {"status": "active"}).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: investigate pod-deletion-cost for k8s autoscaling scale-in (WIN-2028)
* docs: correct worker_instance aggregation claim per review
* docs: drop investigation doc in favor of implementation
* chore: bump ee-repo-ref for pod-deletion-cost autoscaling scale-in
* chore: update ee-repo-ref to 80c39bf7f3bfeda4cf0974ce32826f53affd9574
This commit updates the EE repository reference after PR #610 was merged in windmill-ee-private.
Previous ee-repo-ref: 51f79d4a49dd6491f4809f3edcb3919571719da7
New ee-repo-ref: 80c39bf7f3bfeda4cf0974ce32826f53affd9574
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* docs: replace dead 0x0.st with gh-based PR screenshot recipe
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: harden PR screenshot recipe (filename, secrets, CI fallback)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: make default chat model optional in AI settings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: stop auto-seeding default chat model on provider enable
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(flow): support worker tag override on AI agent steps
* refactor: drop ineffective tag passthrough in nested agent tool path
* chore: regenerate openflow-derived system prompt artifacts
The modal's window keydown-capture handler called preventDefault() and
stopPropagation() on every keystroke while open, which swallowed Cmd/Ctrl+C
and Cmd/Ctrl+V (and blocked typing in any child input). Only intercept
Enter/Escape without modifiers and let all other keys through.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adding or editing environment variables in the worker group config drawer
did not enable the "Apply changes" button. `hasChanges` compares the original
`config` prop against the local `nconfig` copy, but env var edits only mutate
the local `customEnvVars` array — they aren't synced into
`nconfig.env_vars_static`/`env_vars_allowlist` until the Apply handler runs.
This left the button stuck disabled despite real changes.
Add a `hasEnvVarChanges` derived that reconstructs the original env vars from
`config` and compares them to the current `customEnvVars`, and include it in
the Apply button's disabled condition.
Fixes WIN-2023
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai): add list_runs and get_job_logs tools to global chat mode
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ai): always suppress ansi hint in get_job_logs, drop misnamed param
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ai_evals): add global list_runs and get_job_logs eval cases
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(ai): trim get_job_logs description and format global core
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai): surface list_runs/get_job_logs output as tool result
The tools set showDetails but never set message.result, so the details panel rendered "No result yet" even on success. Set result in setToolStatus (logs go in result for get_job_logs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When HTTP request tracing is enabled, the NO_PROXY injected into traced jobs
was built solely from the no_proxy_hosts instance setting, ignoring the
worker container's own NO_PROXY. Enabling tracing therefore silently dropped
every exclusion an operator had already configured at the container level,
funneling those hosts into the MITM proxy (and on to any upstream corporate
proxy). The upstream-relay side already honored the container NO_PROXY; this
makes the injected-into-jobs side symmetric by merging both sources.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): decouple safe local commands from destructive sync push
The schedules, triggers, and resources skill templates lumped every CLI
command under a blunt "do NOT run them yourself" directive. This conflated
two very different risk profiles and forbade the agent from running even
read-only/local commands, creating needless friction.
Align these three with the nuanced policy flow-cli.md already uses: keep
`wmill sync push` defensive (it deploys and can be destructive to remote
state — only run when the user explicitly asks to deploy/publish/push),
while letting read-only commands (`sync pull`, `schedule`, `resource
list`) be run freely. Regenerated auto-generated skills + skills.gen.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): warn that sync push is destructive in dry-run output
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): clarify sync pull mutates local files, not read-only
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: centdix <farhadg110@gmail.com>
* fix(cli): reconcile case-only path drift during sync on case-insensitive filesystems
Windmill paths are case-sensitive, but Windows (and the default macOS
setup) use case-insensitive filesystems. The real-world failure behind
WIN-2020 is not a user authoring both f/Caps and f/caps — it is a single
capitalized folder whose on-disk casing silently drifts (Windows stores
and reports whatever case the directory was first created with,
regardless of the server's path). The diff then sees the drifted local
path as a brand-new item and emits a destructive "delete f/Caps +
add f/caps" pair, so a capitalized folder appears to vanish and a
lowercase clone shows up out of nowhere — and a push can clobber the
real server item.
Fix: on a case-insensitive filesystem, reconcile case-only drift before
diffing. The server's path casing is authoritative, so compareDynFSElement
now rewrites local keys that differ from a remote key only by case to the
server's casing (canonicalizeCaseInsensitiveKeys), making the diff treat
them as the same item. Case-insensitivity is auto-detected by probing the
sync directory, with a WMILL_CASE_INSENSITIVE_FS=true/false override to
force Windows behaviour (or emulate it for tests / cross-platform repos)
on any host. Reconciled paths are summarized in a single info line.
Genuinely unrepresentable collisions — two DISTINCT server paths that
differ only by case — cannot be canonicalized to one target; those are
detected and warned about on every platform so a case-sensitive-Linux
author learns their tree won't round-trip for a Windows/macOS teammate.
Tests:
- Pure unit tests for findCaseInsensitiveCollisions,
canonicalizeCaseInsensitiveKeys and summarizeCaseRewrites (platform
independent).
- An end-to-end drift test that runs on BOTH CI jobs: on the Windows
runner it exercises the real case-insensitive NTFS + auto-probe; on
Linux it reproduces the drift via rename, asserts the destructive
phantom appears without the fix, and asserts a clean no-op push with
the fix forced on.
Fixes WIN-2020
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): canonicalize local-only descendants of drifted folders; dedupe nested case collisions
Address two review findings on the WIN-2020 case-insensitive sync fix:
P1 (correctness): canonicalizeCaseInsensitiveKeys previously only rewrote
local keys with an exact full-path remote match. A brand-new local file
under a drifted folder (e.g. adding f/caps/New.ts when the server has
f/Caps but no f/caps/New.ts) had no exact match, so it kept its lowercase
casing and push uploaded it as-is — recreating f/caps beside f/Caps and
reintroducing the very collision the fix prevents. Canonicalization is now
segment-by-segment against a trie of remote paths, so local-only
descendants inherit the longest unambiguous server folder casing. A segment
is only adopted when the server casing is unambiguous; at the first
ambiguous/unknown segment the remainder keeps local casing. The original
key's separator style is preserved so rewritten keys still round-trip.
P2 (nit): findCaseInsensitiveCollisions reported the folder group AND a
nested per-file group when case-variant folders held same-named files,
inflating the "Found N path(s)" count. It now reports only the shallowest
clash (drops a group whose ancestor prefix is itself a collision).
Tests: add unit coverage for the new-file-under-drifted-folder rewrite, the
stop-at-first-unguided-segment behavior, and shallowest-only collision
reporting; extend the e2e drift test to assert a new item added under the
drifted folder is pushed under the server's folder casing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: deployed↔draft compare for current workspace + session draft bar
Add a "Deployed ↔ draft" comparison alongside the existing fork-vs-parent
compare flow, and surface drafts in the AI session UI.
- Merge the fork-direction toggle (Deploy to parent / Update current) and
the new deployed↔draft mode into one 3-way CompareModeToggle, rendered
inside the comparison card. Hidden in non-fork workspaces (draft only).
- CompareDrafts: list/deploy/discard server drafts (scripts, flows, apps
incl. raw apps) via shared WorkspaceDeployLayout.
- Session draft bar (SessionDraftBar) mirrors the fork bar, only visible
when drafts exist; its diff button opens the shared read-only diff
drawer extracted as WorkspaceDiffDrawer (ForkDiffDrawer + DraftDiffDrawer
are thin wrappers over it).
- WorkspaceDraftsBanner: home banner linking to draft review.
- Backend: GET /drafts/count endpoint for the draft-count badge.
- Raw app draft deploy (rawAppDeploy.ts) + vite /ui_builder proxy headers
so the bundler iframe loads cross-origin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: refine draft/fork compare toggle UX
Follow-up polish on the merged deploy/draft compare control:
- Relabel the draft toggle to "Deploy draft (N)" and show per-direction
counts on all three toggle buttons (deployable / updateable / drafts),
suppressed when zero. Counts are computed page-side so they persist in
draft mode too.
- Warn before deploying to the parent when the fork has undeployed drafts
("Only deployed versions in this fork can be sent to {parent} …") with a
one-click link to the draft view; milder note in the update direction.
- Show an empty-state message per direction ("Nothing to update — this
fork is up to date with {parent}") instead of a table of greyed,
non-actionable rows; hide the deploy/update button in that case.
- Drop the standalone "Pending drafts" info alert from the draft list.
- Align the fork "Show diff" button to the non-deprecated Button API
(unifiedSize, onClick, startIcon) so it matches the draft one; mark
"Discard draft" destructive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: link compare row titles to the item editor
- Render each compare row title (fork and draft) as a link that opens the
item in a new tab, scoped to the current workspace (raw apps route to
/apps_raw/edit), matching the AI-session diff drawer: target=_blank, hover
underline + ExternalLink icon, click stops row-selection propagation.
Kinds without an editor stay plain; the fork rename markup is preserved.
- Drop the "Kind → name" prefix from draft rows — that arrow reads as the
rename visual and the kind is already shown by the row icon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: clickable rows + multi-select in deploy layout
- Make deploy-layout row cards selectable on click via an opt-in
`selectOnRowClick` prop on the shared Row (default off, other tables
unaffected); clicks on the checkbox, title link and action buttons are
ignored. Adds role/tabindex + Enter/Space keyboard support.
- Support multi-select with modifier keys like classic list pickers:
Shift+click selects the contiguous range from the anchor row; Cmd/Ctrl
(and plain) click toggles a single row. select-none avoids text
highlighting on shift-click.
- Turn the "Select all" text into a <label> associated with its checkbox
so clicking the text toggles it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: select all drafts by default in draft compare
Drafts now load pre-selected (deploy-all is the common intent); guarded so
a reload after a deploy doesn't re-select the items left behind. Mirrors
CompareWorkspaces' default auto-selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: show diff for draft-only items stored without a draft row
A draft_only flow/script/app whose content lives in the entity row itself
(created via create*(draft_only: true), no separate draft-table row — like
u/admin/new) returns draft == null from get*ByPathWithDraft. getDraftDiffValues
passed that null through, so the diff "after" side was empty and nothing
rendered. Fall back to the row's own value as the draft content when draft is
null (deployDraft already did this), fixing both the compare-page DiffDrawer
and the session bars' WorkspaceDiffDrawer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: shared diff button across session bars + bar spacing
- Extract SessionDiffButton (variant=default, ± DiffIcon, count, "Open diff"
title) and use it for the diff-drawer trigger in both the fork bar and the
draft bar, so they're identical. Drop the icons from both "Review" buttons.
- Add gap-1 (4px) between the fork bar and draft bar when both are visible
(flex wrapper; single in-flow root per bar, drawer is portalled — no stray
gap when only one shows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: deploying a new (draft-only) flow or app
A draft_only flow/app already has an entity row (created via
create*(draft_only: true)), so deployDraft's createFlow/createApp rejected it
with 400 "already exists". Use updateFlow/updateApp instead — a listed draft
always has a row, and update promotes a draft_only entity to a real deployed
version (clearing the flag), like the editor does. Scripts were unaffected
(createScript + parent_hash makes a new version).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: refresh fork comparison after deploying/discarding a draft
Deploying a draft promotes it to the workspace's deployed version, changing
the fork comparison (ahead/behind vs parent) — but the compare page only
re-fetched it on workspace change, so the deploy/update toggle counts and the
CompareWorkspaces tab went stale. CompareDrafts now fires onChanged after a
successful deploy/discard; the page rewires it to refresh the comparison and
draft count.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: stop draft-count effect from freezing the AI session page
ensureDraftCount cleared its dedupe key on error; since the caller is a reactive $effect (SessionDraftBar), a persistently-failing countDrafts spun the effect into an infinite retry loop that flooded the console and froze the tab. Claim the key before awaiting and keep it set on failure; refresh*() still forces a re-fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: correct draft count and refresh compare counts after actions
count_drafts now counts deployable drafts (draft_only OR has-a-draft-row across script/flow/app), matching the CompareDrafts list, instead of raw draft-table rows which miss new draft-only items. CompareWorkspaces and CompareDrafts fire onChanged so the compare page re-fetches the comparison and draft count after deploy/update/discard, keeping the toggle badges in sync.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: session draft bar shows a fresh count on every (re)open
The runtime persists across client-side navigation, so the deduped
ensureDraftCount() kept a stale count (e.g. a 0 cached before a draft was
created) when a session was re-opened — the bar stayed hidden even though
the server count was >0.
Force one fresh fetch per mount from a non-reactive onMount via
refreshDraftCount(workspace) (which now takes the workspace so it works
before the dedupe key is set). The reactive $effect keeps using
ensureDraftCount: refreshDraftCount reads loadingDraftCount ($state), so
calling it from an effect would track-and-mutate that state into an
infinite fetch loop — ensureDraftCount's plain-key early-return avoids it.
ensureDraftCount also now releases its key after a 5s backoff on failure so
a transient countDrafts error retries instead of leaving the bar stuck.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: make the draft count a single deep Workspace Drafts module
The Draft Count was computed four ways (backend count_drafts SQL, the
CompareDrafts list filter, a bespoke sessionRuntime cache, and the compare
page state) that drifted — the root cause of the unreliable count, the
stale-on-reopen bug, and the effect-loop freeze.
Introduce one module (workspaceDrafts.svelte.ts):
- getDraftItems(ws) lists the deployable Draft Items once; count ≡ list length,
never a separate query.
- useWorkspaceDrafts(() => ws) is a component-scoped runed resource (fetches on
mount + ws change, no persistent cache → fresh on every (re)open).
- invalidateWorkspaceDrafts(ws) refreshes mounted consumers; deployDraft/
discardDraft self-invalidate, so callers never reason about staleness.
Rewire every reader to it (SessionDraftBar, CompareDrafts, DraftDiffDrawer,
WorkspaceDraftsBanner, compare page) and delete the sessionRuntime draftCount
apparatus (key + loading flag + 4 methods + effects + backoff). With no caller
left, remove the count_drafts endpoint (handler, route, openapi, sqlx cache,
generated client) — drafts.rs/openapi return to their main state. Record the
draft vocabulary in CONTEXT.md.
A single GET /w/{ws}/drafts/items endpoint can later replace getDraftItems'
three list calls behind the unchanged seam.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: warn before deploying a draft based on an outdated version
When a newer version is deployed while a draft exists (git-sync/CLI deploys preserve drafts via skip_draft_deletion), the compare/deploy-drafts page now flags the draft as Outdated and gates deploy behind an override confirmation with a diff — instead of silently clobbering the newer version. Staleness is read from the draft's base version: scripts already store parent_hash; flows/apps now record a draft_base_version sidecar on save.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: drop redundant /ui_builder proxyRes hack (superseded by #9433)
main's global configure-response-headers plugin now runs with enforce:'pre'
and sets COOP/COEP/CORP on dev responses (#9433), so the per-proxy proxyRes
override is no longer needed. Revert the /ui_builder block to main's headers
form — vite.config.js now matches main with no branch-specific change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(sessions): keep draft count reactive to preview/chat deploys
Invalidate the Workspace Drafts resource at every frontend deploy seam
(ScriptEditorView / FlowEditorView / RawAppEditorView onDeploy + onSaveDraft)
so user-driven deploys from the Preview panel update the count immediately,
and refresh SessionDraftBar on the same coarse signals SessionForkBar uses
(AI turn-end + tab refocus) to cover chat-driven deploys that happen
server-side and never surface as frontend calls.
Also: always show the draft toggle count including (0) on the compare page,
drop the header/content separator line in both compare cards, and derive the
"Deploy N drafts" footer count so it stays reactive after discard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: address branch review findings
- WorkspaceDraftsBanner: use the modern Button API (variant/unifiedSize/onclick)
instead of the deprecated size/color/on:click triad; drop "pending" from the
banner copy to match CONTEXT.md vocabulary.
- WorkspaceDeployLayout: make Cmd/Ctrl-click distinct from a plain click.
Plain row click now selects only that row (classic file-picker), Cmd/Ctrl
toggles, Shift extends the range, and the checkbox still plain-toggles.
Adds an onSelectOnly callback wired in CompareDrafts/CompareWorkspaces.
- WorkspaceDiffDrawer: document why the file filter is a raw input (bespoke
keyboard-nav integration the design-system inputs can't express).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* revert: drop draft version-gating (stale-draft warning)
Remove the "deploying an outdated draft would override a newer version"
guard. It's a rare edge case and will be handled properly by conflict
resolution in a follow-up PR.
- CompareDrafts: drop staleMap/computeStaleness, the TOCTOU pre-deploy
re-check, the "Outdated" badge, the override-in-diff button, and the
"Newer version deployed" confirmation modal; deploySelected is now the
plain deploy.
- utils_draft_deploy: remove getDraftStaleness/DraftStaleness and the
draft_base_version strip.
- FlowBuilder / AppEditorHeader / RawAppEditorHeader / AppJsonEditor: stop
injecting draft_base_version into draft saves — these editor paths are
back to matching main, shrinking the PR's blast radius.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: unify home banner CTAs on the modern Button API
Both the Workspace Drafts banner and the sibling Fork banner now use
variant="default" unifiedSize="sm" onclick, so the two CTAs on the home
page render identically and neither uses the deprecated size/color/on:click
props.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(compare): show draft deploy direction badge inside forks
Mirror the fork compare header's "from → into" badges on the Deploy-draft
tab: "deploy: draft → into: <fork>". Makes it explicit that deploying a
draft promotes it within the fork (deployed↔draft), not up to the parent.
Only rendered inside forks, where the parent could otherwise be confused
with the deploy target.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(compare): address PR review — dedup drafts resource + shared link
- De-dupe the compare page Workspace Drafts fetch: the page owns the single
resource and passes draftItems/draftsLoading into CompareDrafts (was mounting
a second resource → 6 list calls; now 3).
- Prune transient deploymentStatus for items dropped from the list (no unbounded
growth, no stale 'deployed' suppressing a re-drafted row).
- Type getDraftItems' list fields via a narrow DraftListEntry (drop Array<any>).
- Clear comparison catch-up timers on unmount (onDestroy).
- Extract shared ExternalEditLink.svelte; use it in CompareDrafts,
CompareWorkspaces, WorkspaceDiffDrawer (was a near-verbatim <a> block x3).
- Note conflicts intentionally count in both toggle directions; drop a stray
blank line in sessionRuntime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(compare): show draft summary renames via shared item-summary component
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(compare): address round-2 PR review
- Point the fork-compare edit link at the workspace the item actually lives
in: a parent-only row (absent in the fork) would 404 if linked into the
fork, so link it into the parent instead.
- Replace the bespoke raw <button class="underline">Deploy drafts</button> in
the undeployed-drafts alert with a design-system Button (variant=subtle).
- Drop the stray Prettier reflow in sessionRuntime (restore to match main).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(compare): warn on fork items with a pending draft
In the fork compare list, items that are deployed *and* have a pending
draft (has_draft) now:
- show a yellow "+Draft" badge (AlertTriangle), rendered before the
New/status badges, with a per-direction tooltip explaining that
deploying/updating moves the deployed version, not the draft;
- are excluded from the default selection (still manually selectable);
- trigger a confirmation modal if explicitly selected and deployed/updated,
listing the affected paths.
The signal comes from the page's existing fork drafts resource (a
kind:path Set passed down) — no new fetch, no backend change. Also rename
the undeployed-drafts alert CTA from "Deploy drafts" to "See drafts".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(compare): rename page to "Compare & Deploy"
Update both the page heading (PageHeader) and the browser-tab title.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(compare): multi-select rows by default (no modifier)
In the shared WorkspaceDeployLayout (fork + draft lists), a plain row
click now toggles the item in/out of the selection instead of replacing
the whole selection with it. Removed the modifier-based selection
entirely: the now-dead onSelectOnly path and its two call sites, plus
shift+click range selection (and its anchor/isPickable helpers).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(table): don't toggle row selection on keyboard child activation
Row's onkeydown selection handler lacked the interactive-child guard that
handleRowClick already had, so pressing Enter/Space on a checkbox, action
button, or title link both activated the child and toggled the row's
selection. Extract a shared fromInteractiveChild() guard and apply it in
handleRowKeydown, mirroring the click path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(fork-banner): show draft CTA when fork is up to date
When a fork has no changes vs its parent ("Everything is up to date") but
has pending drafts, the banner now mirrors the non-fork drafts banner:
the status text becomes "This workspace has N draft(s)" and the button
becomes "Review & deploy drafts", linking to the compare page in draft
mode. When the fork has real ahead/behind diffs, the existing status and
buttons are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(compare): honor renamed draft paths + raw-app draft fixes
Address the Codex review:
- Draft deploy now uses the draft payload's path for scripts, flows and raw
apps (keeping the URL path as the existing item key), so a rename in a
draft deploys to the new path instead of silently staying at the old one.
- DraftDiffDrawer maps raw apps to the `raw_app` kind so their row edit
links open the raw-app editor, not the legacy app editor.
- ScriptEditorView.restoreDeployed invalidates the workspace drafts after
deleting the draft, so the session draft-bar count drops immediately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(compare): guard showDiff race, tree label, mode fallback
Address the cubic review:
- CompareDrafts.showDiff uses a monotonic request token so two quick "Show
diff" clicks can't let a slow earlier fetch overwrite a faster later one.
- WorkspaceDiffDrawer.buildTree labels a 2-segment path with its leaf name
(parts[1]) instead of the full scope key.
- The compare page only resolves ?mode=draft immediately; ?mode=fork (and
an absent mode) defer to the isFork-aware effect, which falls back to
draft for non-fork workspaces instead of stranding them on the fork UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: remove CONTEXT.md from the PR
Drop the root CONTEXT.md domain glossary and the lone comment pointer to
it in workspaceDrafts.svelte.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(compare): send custom_path on raw-app draft deploy
A raw-app draft that changes or clears its custom route was silently
dropped on deploy from the compare page: updateAppRaw omitted custom_path,
so the backend preserved the old route. Send the draft's custom_path on
update — matching the fork deploy path (which spreads the full app,
custom_path included) and the createAppRaw branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): refresh draft count on raw-app session save-draft
The script/flow session editors invalidate the workspace drafts on
save-draft, but the raw-app editor only did so on deploy. Thread an
onSaveDraft callback through RawAppEditor → RawAppEditorHeader and call
invalidateWorkspaceDrafts from RawAppEditorView, so saving a raw-app draft
in an AI session updates the SessionDraftBar count immediately (and the bar
appears when the count was zero).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(compare): honor renamed paths, draft triggers & paginate inventory
Address the Codex review:
- Draft deploy honors the draft's renamed path for scripts/flows/raw apps
(keeping the URL path as the existing item key).
- Script/flow draft deploy now deploys draft_triggers via the shared
deployTriggers, instead of silently dropping them with the draft.
- rawAppDeploy sends custom_path admin-gated on update (admin: value/'' to
clear; non-admin: undefined) so non-admins don't hit RequireAdmin.
- getDraftItems pages through listScripts/listFlows/listApps so drafts past
the first page are included in the count, banners, drawer and deploy list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(compare): admin-gate custom_path on visual-app draft deploy
The visual-app branch of deployDraft sent custom_path unconditionally on
updateApp, so a non-admin deploying an app draft for an app with a custom
route hit RequireAdmin. Mirror AppEditorHeader and the raw-app path: admins
send the draft's custom_path ('' clears), non-admins send undefined so the
backend preserves the existing route.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(raw-app): save initial draft directly when path is known
In the AI-session preview, a never-deployed raw app has newApp=true but a
known path, so saveDraft opened the "Initial draft save" path-picker drawer
— which is gated on `appPath == ''` and therefore never rendered, making
Save draft silently do nothing. Branch the new-app case on appPath: pick a
path via the drawer only when none is chosen yet; otherwise call
saveInitialDraft() directly. saveInitialDraft now also toasts and fires
onSaveDraft so the session draft-bar count refreshes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(compare): preserve deployed custom_path on visual-app draft deploy
The visual-app draft value usually omits custom_path, so the admin branch's
`d.custom_path ?? ''` sent an empty string, which the backend treats as
"clear the route" — an admin deploying a content-only draft would wipe the
app's existing custom route. Fall back to the deployed route
(`r.custom_path`) when the draft omits it; an explicit '' still clears.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(api): add endpoint to update token label
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(api): prevent renaming the session token label
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(api): restrict token-label edits to user tokens, not just session
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): edit token label in the edit modal instead of inline
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(api): reject relabeling tokens to reserved system-token names
Centralize the is_user_token classifier in windmill-common and reuse it
to reject labels colliding with system-token namespaces (ephemeral*,
debugger-token, mcp-oauth-*), not just session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(api): match ephemeral label case-insensitively and cap label length
Align the canonical is_user_token, the SQL guard and the frontend mirror on
a case-insensitive `ephemeral` match (so a token can't be relabeled to a
casing the backend allows but the UI hides), reject labels over the
VARCHAR(1000) column limit with a 400, and add unit tests for is_user_token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>