mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
docs: tighten global ai userdraft requirements
This commit is contained in:
@@ -2,285 +2,211 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
Global mode AI chat needs to inspect and modify workspace items without
|
||||
immediately deploying them. For frontend-only draft state, it should use the
|
||||
same `UserDraft` service as editors, but with a stricter contract:
|
||||
This document enumerates the hard requirements that global mode AI chat places
|
||||
on the frontend `UserDraft` service. It is intentionally limited to required
|
||||
behavior and does not prescribe API names, data structures, or implementation
|
||||
strategy.
|
||||
|
||||
- editor state must remain safe from accidental deletion or deployment;
|
||||
- AI-written drafts must be discoverable and deployable;
|
||||
- live editor handles and persisted localStorage entries must stay consistent;
|
||||
- backend draft-only items must not be confused with frontend `UserDraft`
|
||||
entries.
|
||||
The requirements are based on the current base branch state of `UserDraft`:
|
||||
|
||||
This document defines the requirements for that contract. It is intended to
|
||||
guide a small UserDraft-only PR before wiring global mode AI chat to those
|
||||
methods.
|
||||
|
||||
## Terms
|
||||
|
||||
### UserDraft
|
||||
|
||||
The frontend local draft service backed by localStorage and optional live
|
||||
Svelte handles. It is scoped by `(workspace, itemKind, storagePath)`.
|
||||
|
||||
### Storage path
|
||||
|
||||
The path used in the localStorage key and live-handle map. For existing items
|
||||
this is normally the item path. For new-item editors, this can be an empty
|
||||
string while the draft value already contains its final workspace path.
|
||||
|
||||
### Item path
|
||||
|
||||
The workspace path inside the draft value, for example
|
||||
`f/scripts/my_script`. Global mode must operate by item path.
|
||||
|
||||
### Entry source
|
||||
|
||||
Where the current `UserDraft` value came from:
|
||||
|
||||
- `persisted`: localStorage only;
|
||||
- `live`: live editor handle only;
|
||||
- `both`: live handle plus localStorage entry.
|
||||
|
||||
This is not the same as draft ownership.
|
||||
|
||||
### Draft origin
|
||||
|
||||
Who wrote the value:
|
||||
|
||||
- editor-originated draft: editor autosave or editor baseline hydration;
|
||||
- external-originated draft: global mode AI chat, or any future external actor
|
||||
that writes into UserDraft outside the editor's normal mutation flow.
|
||||
|
||||
Only external-originated drafts are deployable by global AI mode.
|
||||
- drafts are scoped by workspace, item kind, and storage path;
|
||||
- persisted drafts are stored in localStorage under `userdraft/w/...`;
|
||||
- live editor handles may hold draft state that is not present in
|
||||
localStorage;
|
||||
- draft values are wrapped with freshness metadata used by editor staleness
|
||||
checks;
|
||||
- existing editor flows rely on `UserDraft` for autosave, restore, metadata,
|
||||
removal, discard, and live-handle behavior;
|
||||
- the service does not provide a public way to enumerate all draft entries;
|
||||
- the service does not identify whether a draft was written by an editor or by
|
||||
an external actor.
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1. Preserve the existing editor contract
|
||||
### R1. Existing Editor Behavior Must Be Preserved
|
||||
|
||||
Existing editor usage of `UserDraft.save`, `UserDraft.saveIfChanged`,
|
||||
`UserDraft.use`, `UserDraft.useMany`, `setDraftAndMeta`, and `setMeta` must keep
|
||||
working without global mode installed.
|
||||
All existing editor behavior that depends on `UserDraft` must remain unchanged.
|
||||
|
||||
Opening an editor and hydrating its current backend state must not create a
|
||||
deployable AI draft. A clean open editor baseline is useful read context, but it
|
||||
is not unsaved AI work.
|
||||
Opening an editor, hydrating current backend state, autosaving local edits,
|
||||
restoring local edits, checking staleness, removing local drafts, and discarding
|
||||
to a fallback value must keep the same observable behavior.
|
||||
|
||||
### R2. External writes must be explicit
|
||||
Global mode must not cause a clean editor baseline to become deployable AI work.
|
||||
|
||||
Global mode must not call the normal editor-oriented `UserDraft.save` when it
|
||||
creates or updates an AI draft. It needs an explicit method such as:
|
||||
### R2. Global Mode Writes Must Be Distinguishable From Editor Writes
|
||||
|
||||
```ts
|
||||
UserDraft.saveExternal(itemKind, storagePath, value, opts)
|
||||
```
|
||||
When global mode creates or updates a frontend local draft, later code must be
|
||||
able to distinguish that draft from drafts created by normal editor autosave or
|
||||
editor hydration.
|
||||
|
||||
That method must:
|
||||
This distinction must survive page reloads.
|
||||
|
||||
- mark metadata so the entry can later be recognized as external-originated;
|
||||
- preserve existing freshness metadata such as `remoteRev` and
|
||||
`remoteDraftRev`;
|
||||
- update any live handle for the same key immediately;
|
||||
- persist to localStorage immediately, even if a live handle exists.
|
||||
This distinction must survive freshness metadata updates.
|
||||
|
||||
Immediate persistence matters because global mode listing, deployment, and page
|
||||
refresh recovery must see the AI-written draft without waiting for an editor
|
||||
autosave effect.
|
||||
Legacy drafts with no such distinction must not be treated as global mode AI
|
||||
drafts.
|
||||
|
||||
### R3. Normal editor saves must remain neutral
|
||||
### R3. External Draft Writes Must Be Visible Immediately
|
||||
|
||||
`UserDraft.save` and `UserDraft.saveIfChanged` must not mark drafts as external.
|
||||
Absence of external-origin metadata must be treated as editor-originated or
|
||||
legacy state.
|
||||
After global mode writes a frontend local draft, the written value must be
|
||||
visible immediately to:
|
||||
|
||||
This keeps existing localStorage entries backward-compatible and prevents old
|
||||
editor drafts from becoming deployable by global mode.
|
||||
- any live editor handle for the same workspace, item kind, and storage path;
|
||||
- any localStorage-based lookup for that draft;
|
||||
- any global mode read, list, deploy, or delete flow that runs after the write.
|
||||
|
||||
### R4. Listing must include enough metadata for safe filtering
|
||||
This must hold even when a live editor handle already exists before the write.
|
||||
|
||||
Global mode needs a generic listing API, for example:
|
||||
### R4. Freshness Metadata Must Not Be Lost
|
||||
|
||||
```ts
|
||||
UserDraft.list({ workspace, itemKinds })
|
||||
```
|
||||
Global mode writes and any later metadata updates must preserve the freshness
|
||||
metadata needed by existing editor staleness checks.
|
||||
|
||||
Each entry must include:
|
||||
Freshness metadata changes must not erase the marker that distinguishes global
|
||||
mode drafts from editor drafts.
|
||||
|
||||
- `workspace`;
|
||||
- `itemKind`;
|
||||
- `storagePath`;
|
||||
- best-effort `path` for the storage key;
|
||||
- `source`: `persisted`, `live`, or `both`;
|
||||
- `draftOrigin` or equivalent metadata;
|
||||
- cloned `value`.
|
||||
The marker that distinguishes global mode drafts from editor drafts must not
|
||||
erase freshness metadata.
|
||||
|
||||
The listing API must merge persisted and live entries for the same key. It must
|
||||
not expose the same draft twice.
|
||||
### R5. Draft Enumeration Must Be Possible
|
||||
|
||||
### R5. Listing must tolerate live editor runtime values
|
||||
Global mode must be able to enumerate frontend local drafts for a workspace and
|
||||
a bounded set of item kinds.
|
||||
|
||||
Some live editor values contain functions, class instances, or other
|
||||
non-structured-cloneable fields. `UserDraft.list` must still return the usable
|
||||
draft fields instead of crashing.
|
||||
Enumeration must include persisted-only drafts.
|
||||
|
||||
A safe implementation can try `$state.snapshot` and `structuredClone`, then fall
|
||||
back to a shallow/object-preserving clone when a runtime-only field cannot be
|
||||
cloned.
|
||||
Enumeration must include live-only draft values.
|
||||
|
||||
### R6. Storage path and item path must stay separate
|
||||
Enumeration must include draft values that exist both in localStorage and in a
|
||||
live editor handle.
|
||||
|
||||
Global mode works by workspace item path, but UserDraft storage is keyed by
|
||||
storage path. UserDraft must expose storage path in list entries so callers can
|
||||
later delete or overwrite the exact stored draft.
|
||||
Enumeration must not return duplicate logical entries for the same workspace,
|
||||
item kind, and storage path.
|
||||
|
||||
This is required for new-item editors where:
|
||||
### R6. Enumeration Must Expose Storage Identity
|
||||
|
||||
- storage path can be empty;
|
||||
- draft value path can be `u/admin/new_script`;
|
||||
- global mode must find the item by value path;
|
||||
- delete/clear must target the original storage path.
|
||||
For each enumerated draft, global mode must be able to identify the exact
|
||||
workspace, item kind, and storage path used by `UserDraft`.
|
||||
|
||||
### R7. External drafts must be deployable only when persisted
|
||||
This is required because global mode operates on workspace item paths, while
|
||||
`UserDraft` storage paths are not always equal to item paths.
|
||||
|
||||
Global mode should consider an entry deployable only if:
|
||||
New-item editor drafts with an empty storage path must remain representable.
|
||||
|
||||
- it is external-originated; and
|
||||
- it is persisted (`source` is `persisted` or `both`).
|
||||
### R7. Enumeration Must Expose Persistence State
|
||||
|
||||
A live-only value can be read as current context, but deploying a live-only value
|
||||
is risky because it may be an editor baseline or a transient state that would be
|
||||
lost on refresh.
|
||||
For each enumerated draft, global mode must be able to determine whether the
|
||||
value exists in localStorage, in a live editor handle, or in both.
|
||||
|
||||
### R8. Reads must distinguish current context from deployable drafts
|
||||
Global mode must not need to infer persistence state by reading private
|
||||
`UserDraft` internals.
|
||||
|
||||
Global mode needs two read concepts:
|
||||
### R8. Enumeration Must Expose Draft Origin
|
||||
|
||||
- current frontend-visible item: includes clean live editor baselines and
|
||||
editor drafts, and is useful as context;
|
||||
- deployable AI draft: external-originated and persisted.
|
||||
For each enumerated draft, global mode must be able to determine whether the
|
||||
draft is global-mode-originated or editor-originated.
|
||||
|
||||
The UserDraft API should provide enough metadata for a caller to implement both
|
||||
views without guessing from the item value alone.
|
||||
Global mode must not need to infer origin from the draft value shape, item path,
|
||||
item kind, or localStorage key.
|
||||
|
||||
### R9. Clear must reset persisted and live state
|
||||
### R9. Live-Only Values Must Not Be Deployable By Global Mode
|
||||
|
||||
Global mode delete/clear needs a method such as:
|
||||
A frontend local draft must not be deployable by global mode unless it is known
|
||||
to be global-mode-originated and persisted.
|
||||
|
||||
```ts
|
||||
UserDraft.clear(itemKind, storagePath, opts)
|
||||
```
|
||||
Live-only values may be read as current frontend-visible context, but they must
|
||||
not be treated as deployable AI drafts.
|
||||
|
||||
The method must:
|
||||
### R10. Current Context And Deployable Drafts Must Be Separatable
|
||||
|
||||
- remove the localStorage entry;
|
||||
- reset any live handle for that key immediately;
|
||||
- avoid re-persisting the cleared value via the live handle's autosave effect.
|
||||
Global mode must be able to distinguish:
|
||||
|
||||
The existing `remove` method is not enough for global mode because it clears
|
||||
localStorage only and can leave stale live state visible.
|
||||
- current frontend-visible local state, which may include clean editor
|
||||
baselines, editor-originated drafts, live-only values, and global mode drafts;
|
||||
- deployable global mode draft state, which must include only persisted
|
||||
global-mode-originated drafts.
|
||||
|
||||
### R10. Discard with fallback must remain available for editors
|
||||
This distinction must be available without inspecting private `UserDraft`
|
||||
internals.
|
||||
|
||||
Editors still need the ability to discard local changes and restore a deployed
|
||||
baseline in memory:
|
||||
### R11. Clearing A Global Mode Draft Must Clear Persisted And Live State
|
||||
|
||||
```ts
|
||||
UserDraft.discard(itemKind, storagePath, fallback, opts)
|
||||
```
|
||||
When global mode clears a frontend local draft, the draft must be removed from
|
||||
localStorage and from any live editor handle for the same workspace, item kind,
|
||||
and storage path.
|
||||
|
||||
`clear` should be a convenience wrapper for `discard(..., undefined)`, not a
|
||||
replacement for editor discard behavior.
|
||||
The cleared live state must not be re-persisted automatically as a side effect
|
||||
of clearing.
|
||||
|
||||
### R11. Metadata updates must preserve ownership metadata
|
||||
Clearing a global mode draft must not clear unrelated editor drafts or unrelated
|
||||
workspace items.
|
||||
|
||||
`saveMeta` and handle-level `setMeta` must merge metadata instead of replacing
|
||||
the entire metadata object. Updating `remoteRev` or `remoteDraftRev` must not
|
||||
drop the external-origin marker.
|
||||
### R12. Existing Discard Semantics Must Remain Available
|
||||
|
||||
Similarly, `saveExternal` must not drop existing remote freshness metadata.
|
||||
Editor flows must retain the ability to discard local draft state while
|
||||
restoring a provided fallback value in memory.
|
||||
|
||||
### R12. UserDraft must stay domain-generic
|
||||
Global mode clearing requirements must not remove or weaken that editor
|
||||
discard behavior.
|
||||
|
||||
UserDraft should not know about `WorkspaceItem`, scripts, flows, triggers, apps,
|
||||
or global AI chat tools. It should expose generic draft entries and metadata.
|
||||
### R13. Enumeration Must Be Robust To Editor Runtime Values
|
||||
|
||||
Mapping `UserDraftItemKind` to global mode workspace item types should live in a
|
||||
global-mode adapter layer.
|
||||
Draft enumeration must not fail because a live editor draft contains
|
||||
runtime-only values that cannot be serialized into localStorage.
|
||||
|
||||
### R13. Backend DB drafts are outside UserDraft
|
||||
Enumeration may omit runtime-only details, but it must preserve the draft data
|
||||
needed for global mode to identify, read, list, and clear the draft.
|
||||
|
||||
Backend draft-only scripts, flows, and apps are not frontend UserDraft entries.
|
||||
Global mode read/list tools must use backend draft-aware endpoints for those.
|
||||
### R14. UserDraft Must Remain Domain-Agnostic
|
||||
|
||||
UserDraft should only represent local frontend drafts. The global-mode adapter
|
||||
is responsible for merging:
|
||||
`UserDraft` must remain independent of global mode workspace item semantics.
|
||||
|
||||
- backend deployed items;
|
||||
- backend DB draft-only items;
|
||||
- local UserDraft current context;
|
||||
- local UserDraft external drafts.
|
||||
It must not need to know about scripts, flows, apps, schedules, triggers,
|
||||
resources, variables, deployment behavior, or chat tools.
|
||||
|
||||
### R14. Tests must cover live/persisted edge cases
|
||||
Global mode-specific interpretation of item kinds and draft values must remain
|
||||
outside `UserDraft`.
|
||||
|
||||
The UserDraft-only PR should include unit tests for:
|
||||
### R15. Backend Drafts Must Remain Separate From UserDraft
|
||||
|
||||
- `saveExternal` marks origin and persists without a live handle;
|
||||
- `saveExternal` updates and persists when a live handle exists;
|
||||
- normal `save` does not mark origin;
|
||||
- `list` reports `storagePath`, `source`, origin, and cloned value;
|
||||
- `list` handles persisted-only, live-only, and both states;
|
||||
- `clear` removes localStorage and clears live state;
|
||||
- `discard` still restores a fallback baseline;
|
||||
- metadata updates preserve origin;
|
||||
- uncloneable live values do not make `list` throw;
|
||||
- empty storage path plus value-level item path remains representable.
|
||||
Backend draft-only scripts, flows, and apps are not frontend `UserDraft`
|
||||
entries.
|
||||
|
||||
Global mode integration tests should live in the later global-mode PR, not in
|
||||
the UserDraft-only PR.
|
||||
Global mode must not require `UserDraft` to represent backend DB drafts.
|
||||
|
||||
## Proposed UserDraft-only PR Scope
|
||||
Global mode must be able to combine backend draft-aware reads with frontend
|
||||
`UserDraft` state without treating one storage system as the other.
|
||||
|
||||
The first PR should be limited to UserDraft service primitives and tests.
|
||||
### R16. Cross-Workspace Isolation Must Be Preserved
|
||||
|
||||
### In scope
|
||||
Draft reads, writes, enumeration, and clearing must remain scoped to the target
|
||||
workspace.
|
||||
|
||||
- Add explicit external-write API.
|
||||
- Add origin metadata to stored draft metadata.
|
||||
- Enrich `UserDraft.list` entries with `storagePath`, `source`, and origin.
|
||||
- Make `UserDraft.list` merge persisted and live entries safely.
|
||||
- Add `UserDraft.clear`.
|
||||
- Ensure metadata merge behavior preserves origin.
|
||||
- Add focused `frontend/src/lib/userDraft.test.ts` coverage.
|
||||
Global mode must not see, modify, deploy, or clear drafts from another
|
||||
workspace.
|
||||
|
||||
### Out of scope
|
||||
### R17. Item-Kind Boundaries Must Be Preserved
|
||||
|
||||
- No global chat tool wiring.
|
||||
- No changes to global AI prompts.
|
||||
- No global draft inspector route.
|
||||
- No deploy behavior changes.
|
||||
- No script/flow/app-specific adapter logic.
|
||||
- No backend API changes.
|
||||
Draft reads, writes, enumeration, and clearing must remain scoped to the target
|
||||
item kind.
|
||||
|
||||
## Later Global Mode PR Scope
|
||||
Global mode must not treat a draft of one item kind as a draft of another item
|
||||
kind.
|
||||
|
||||
The follow-up global-mode PR can then:
|
||||
### R18. Regression Coverage Is Required
|
||||
|
||||
- introduce a thin adapter that maps UserDraft entries to global
|
||||
`WorkspaceItem`s;
|
||||
- use external writes for global mode `write_*` tools;
|
||||
- list external persisted drafts as deployable;
|
||||
- read local current context without marking clean editor baselines as drafts;
|
||||
- clear AI drafts through `UserDraft.clear`;
|
||||
- merge backend items, backend draft-only items, editor context, and AI drafts
|
||||
deterministically;
|
||||
- add global chat unit and browser tests for read/list/write/deploy flows.
|
||||
The UserDraft-only change must include tests covering:
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
The UserDraft-only PR is acceptable when:
|
||||
|
||||
- existing editor behavior is unchanged;
|
||||
- external-origin drafts can be identified without inspecting item shape;
|
||||
- deployable vs current-context decisions can be made from list metadata;
|
||||
- live handles and localStorage cannot diverge after external writes or clears;
|
||||
- no global mode code needs private access to UserDraft internals.
|
||||
- preservation of existing editor behavior;
|
||||
- distinction between editor-originated and global-mode-originated drafts;
|
||||
- persisted-only draft enumeration;
|
||||
- live-only draft enumeration;
|
||||
- combined persisted and live draft enumeration without duplicates;
|
||||
- storage path exposure when storage path differs from item path;
|
||||
- global-mode-originated writes becoming visible to live handles and
|
||||
localStorage;
|
||||
- clearing removing both persisted and live state;
|
||||
- freshness metadata preservation;
|
||||
- cross-workspace isolation;
|
||||
- item-kind isolation;
|
||||
- enumeration robustness when live draft values contain runtime-only data.
|
||||
|
||||
Reference in New Issue
Block a user