feat: read local userdraft state in global ai

This commit is contained in:
centdix
2026-05-19 16:00:47 +02:00
parent 9d90c91df4
commit 308666c019
3 changed files with 343 additions and 73 deletions
@@ -26,6 +26,14 @@ vi.mock('@codingame/monaco-vscode-languages-service-override', () => ({
vi.mock('$lib/components/vscode', () => ({}))
vi.mock('svelte', async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>
return {
...actual,
onDestroy: vi.fn()
}
})
vi.mock('$lib/gen', async () => {
const actual = await vi.importActual<any>('$lib/gen')
@@ -93,6 +101,11 @@ vi.mock('$lib/gen', async () => {
AppService: wrapService(actual.AppService, {
existsApp: vi.fn(async () => false)
}),
ResourceService: wrapService(actual.ResourceService, {
existsResource: vi.fn(async () => false),
listResource: vi.fn(async () => []),
queryResourceTypes: vi.fn(async () => [])
}),
VariableService: wrapService(actual.VariableService, {
existsVariable: vi.fn(async () => false),
getVariable: vi.fn(async ({ path }: { path: string }) => ({
@@ -146,11 +159,40 @@ describe('global AI tools', () => {
vi.clearAllMocks()
})
it('does not expose resource or variable draft writers until their UserDraft contracts are complete', () => {
const toolNames = globalTools.map((tool) => tool.def.function.name)
it('writes resource and variable drafts into the shared UserDraft store', async () => {
await callGlobalTool('write_variable', {
path: 'f/secrets/api_key',
value: 'secret',
is_secret: true,
description: 'API key'
})
await callGlobalTool('write_resource', {
path: 'f/resources/api',
resource_type: 'http',
description: 'API resource',
value: {
token: '$var:f/secrets/api_key'
}
})
expect(toolNames).not.toContain('write_resource')
expect(toolNames).not.toContain('write_variable')
expect(
UserDraft.get<any>('variable', 'f/secrets/api_key', { workspace: WORKSPACE })
).toMatchObject({
path: 'f/secrets/api_key',
value: 'secret',
is_secret: true,
description: 'API key'
})
expect(
UserDraft.get<any>('resource', 'f/resources/api', { workspace: WORKSPACE })
).toMatchObject({
path: 'f/resources/api',
resource_type: 'http',
description: 'API resource',
value: {
token: '$var:f/secrets/api_key'
}
})
})
it('writes script drafts into the shared UserDraft store', async () => {
@@ -182,10 +224,39 @@ describe('global AI tools', () => {
summary: 'Hello script',
language: 'bun',
value: content,
isDraft: true
isDraft: false
})
})
it('reads a live UserDraft editor baseline as current context without marking it draft', async () => {
const content = 'export async function main() {\n\treturn "open editor"\n}'
const handle = UserDraft.use<any>('script', 'f/scripts/open-editor', { workspace: WORKSPACE })
handle.setDraftAndMeta(
{
path: 'f/scripts/open-editor',
summary: 'Open editor baseline',
language: 'bun',
content
},
{ remoteRev: 'v1' }
)
const raw = await callGlobalTool('read_workspace_item', {
type: 'script',
path: 'f/scripts/open-editor'
})
expect(JSON.parse(raw)).toMatchObject({
type: 'script',
path: 'f/scripts/open-editor',
summary: 'Open editor baseline',
language: 'bun',
value: content,
isDraft: false
})
expect(localStorage.getItem(`userdraft/w/${WORKSPACE}/script/f/scripts/open-editor`)).toBeNull()
})
it('overlays shared UserDraft entries in list_workspace_items', async () => {
await callGlobalTool('write_script', {
path: 'f/scripts/listed',
@@ -203,7 +274,7 @@ describe('global AI tools', () => {
type: 'script',
path: 'f/scripts/listed',
summary: 'Listed script',
isDraft: true
isDraft: false
})
])
})
@@ -242,7 +313,7 @@ describe('global AI tools', () => {
schedule: '0 0 12 * * *',
timezone: 'UTC'
}),
isDraft: true
isDraft: false
})
})
@@ -280,7 +351,7 @@ describe('global AI tools', () => {
triggerKind: 'http',
path: 'f/triggers/hook',
summary: 'Hook trigger',
isDraft: true
isDraft: false
})
])
})
@@ -405,6 +476,7 @@ describe('global AI tools', () => {
})
const item = JSON.parse(raw)
expect(item.isDraft).toBe(false)
expect(item.value).toMatchObject({
modules: [
{
@@ -61,8 +61,10 @@ import {
} from '../shared'
import type { ContextElement } from '../context'
import {
resourceRequestSchema,
scheduleRequestSchema,
triggerRequestSchemas
triggerRequestSchemas,
variableRequestSchema
} from '../workspaceToolsZod.gen'
import {
getWorkspaceItemKey,
@@ -77,8 +79,9 @@ import {
import { buildFlowDeployRequestBody, buildScriptDeployRequestBody } from './deployRequests'
import {
deleteGlobalDraft,
getGlobalCurrentItem,
getGlobalDraft,
listGlobalDrafts,
listGlobalCurrentItems,
triggerKindToUserDraftKind
} from './userDraftAdapter'
@@ -276,6 +279,10 @@ const writeTriggerSchema = z.object({
)
})
const writeResourceSchema = resourceRequestSchema
const writeVariableSchema = variableRequestSchema
const searchResourceTypesSchema = z.object({
query: z.string().describe('Substring to match against resource type names.'),
limit: z
@@ -467,7 +474,7 @@ const initAppSchema = z.object({
const GLOBAL_SYSTEM_PROMPT = `You are Windmill's global workspace assistant.
You can inspect workspace scripts, flows, schedules, triggers, resources, variables, and apps, then create draft changes in the frontend AI draft store for supported item types.
You can inspect workspace scripts, flows, schedules, triggers, resources, variables, and apps, then create normal local UserDraft changes for supported item types.
Important rules:
- write_{script,flow,schedule,trigger} create or overwrite drafts. They do not save, deploy, or mutate workspace items.
@@ -476,14 +483,14 @@ Important rules:
- deploy_workspace_item persists a draft to the workspace via the real backend create/update API and removes the draft. Requires user confirmation. Only call after the user has reviewed the draft and explicitly asked to deploy.
- delete_workspace_item permanently removes a workspace item (and any matching draft). Irreversible. Requires user confirmation. Only call when the user has explicitly asked to delete.
- Use list_workspace_items before broad reads.
- Use read_workspace_item before overwriting an existing item, unless the user already provided the complete current item. For triggers, pass trigger_kind.
- Use read_workspace_item before overwriting an existing item, unless the user already provided the complete current item. It returns the freshest frontend-visible value: local UserDraft state when present, otherwise backend state. For triggers, pass trigger_kind.
- Variable values are NEVER returned by read_workspace_item or list_workspace_items — only metadata (path, description, is_secret). The model cannot read secret values, by design.
- For resources that need secrets, write a Variable first (with is_secret: true), then in the resource value reference it as "$var:path/to/variable". When deploying both, deploy the variable before the resource.
- Use search_resource_types before write_resource to discover the resource_type name and the JSON Schema its value must match.
- Use get_instructions before writing a script, flow, resource, or app. For scripts, pass the target language; when modifying, use the language from the item you read.
- Schedules, triggers, and variables do not need get_instructions — their tool schemas describe every field.
- When a required decision is ambiguous, use askUserQuestion with two to six clear answer strings instead of guessing.
- A workspace item is { type, path, summary?, language?, triggerKind?, value, isDraft }. For scripts, value is the source code string. For flows, read_workspace_item returns value as the compact flow object { modules, schema, preprocessor_module, failure_module, groups }; write_flow takes the same flow fields as top-level tool arguments plus path/summary. For schedules/triggers/resources/variables, value is the full request body for that type. For apps, value is { files, runnables, data?, policy?, custom_path? } with frontend file contents and backend runnable definitions.
- A workspace item is { type, path, summary?, language?, triggerKind?, value, isDraft }. For AI read context, local UserDraft state is not always marked as isDraft because it may be a clean open-editor baseline rather than an unsaved change. For scripts, value is the source code string. For flows, read_workspace_item returns value as the compact flow object { modules, schema, preprocessor_module, failure_module, groups }; write_flow takes the same flow fields as top-level tool arguments plus path/summary. For schedules/triggers/resources/variables, value is the full request body for that type. For apps, value is { files, runnables, data?, policy?, custom_path? } with frontend file contents and backend runnable definitions.
- Apps (raw apps): use list_workspace_items with types: ['app'] to find them, read_workspace_item with type 'app' for a metadata summary (file paths + runnable list, no contents), then read_app_file to read individual files. Edit with write_app_file / patch_app_file / delete_app_file for frontend files and write_app_runnable / delete_app_runnable for backend runnables. Frontend file paths start with "/" (e.g. /index.tsx). Backend inline runnables are addressed as "backend/<key>/main.{ts|py}". /wmill.d.ts is generated and cannot be written.
- To create a new raw app, use init_app. Before calling it, confirm framework (react19 / react18 / svelte5 / vue), path, and summary with the user — do not silently default to react19, even though it is the recommended choice.
- Apps cannot be deployed from chat. The app editor bundles JS/CSS before save; tell the user to open the app editor to deploy app drafts.
@@ -818,9 +825,14 @@ function getInlineRunnableContent(
}
async function loadAppDraftValue(path: string, workspace: string): Promise<AppDraftValue> {
const draft = getGlobalDraft(workspace, 'app', path)
if (draft && draft.value && typeof draft.value === 'object' && 'files' in draft.value) {
return draft.value as AppDraftValue
const currentItem = getGlobalCurrentItem(workspace, 'app', path)
if (
currentItem &&
currentItem.value &&
typeof currentItem.value === 'object' &&
'files' in currentItem.value
) {
return currentItem.value as AppDraftValue
}
const app = await AppService.getAppByPath({ workspace, path })
@@ -1138,7 +1150,7 @@ ${getRawAppPrompt()}`
function getResourceInstructions(): string {
return `# Resource & variable reference
- Global mode can inspect resources and variables, but cannot create resource or variable drafts until their editor-native UserDraft contracts are complete.
- Global mode can create local resource and variable drafts. Drafts are not deployed until the user explicitly asks to deploy them.
- For secret fields in a resource value, do NOT inline the raw secret. Reference variables as \`"$var:path/to/variable"\`.
- Reference formats inside resource values: \`$var:g/all/name\` (global), \`$var:u/user/name\` (user), \`$var:f/folder/name\` (folder). Reference another resource with \`$res:path/to/resource\`.
- Use \`search_resource_types\` to discover valid \`resource_type\` names and their JSON Schemas. Match the resource value to that schema.
@@ -1237,7 +1249,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
listWorkspaceItemsSchema,
'list_workspace_items',
'List workspace items (scripts, flows, schedules, triggers, resources, variables, apps) and AI drafts. Returns metadata only (no value). Defaults to scripts and flows.'
'List workspace items (scripts, flows, schedules, triggers, resources, variables, apps) and local UserDraft entries. Returns metadata only (no value). Defaults to scripts and flows.'
),
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
const parsed = listWorkspaceItemsSchema.parse(args)
@@ -1256,12 +1268,15 @@ export const globalTools: Tool<{}>[] = [
byKey.set(getWorkspaceItemKey(item.type, item.path, item.triggerKind), item)
}
for (const draft of listGlobalDrafts(workspace)) {
if (!types.includes(draft.type)) continue
byKey.set(getWorkspaceItemKey(draft.type, draft.path, draft.triggerKind), {
...draft,
value: undefined
})
for (const currentItem of listGlobalCurrentItems(workspace)) {
if (!types.includes(currentItem.type)) continue
byKey.set(
getWorkspaceItemKey(currentItem.type, currentItem.path, currentItem.triggerKind),
{
...currentItem,
value: undefined
}
)
}
const results = Array.from(byKey.values())
@@ -1278,7 +1293,7 @@ export const globalTools: Tool<{}>[] = [
def: createToolDef(
readWorkspaceItemSchema,
'read_workspace_item',
'Read one workspace item or AI draft by type and path. Returns the full workspace item including value.'
'Read the freshest frontend-visible value for one workspace item by type and path. Uses local UserDraft state when present, otherwise reads the backend item.'
),
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
const parsed = readWorkspaceItemSchema.parse(args)
@@ -1287,12 +1302,17 @@ export const globalTools: Tool<{}>[] = [
toolCallbacks.setToolStatus(toolId, { content: message, error: message })
return JSON.stringify({ success: false, error: message })
}
const draft = getGlobalDraft(workspace, parsed.type, parsed.path, parsed.trigger_kind)
if (draft) {
const currentItem = getGlobalCurrentItem(
workspace,
parsed.type,
parsed.path,
parsed.trigger_kind
)
if (currentItem) {
toolCallbacks.setToolStatus(toolId, {
content: `Read AI draft ${parsed.type} "${parsed.path}"`
content: `Read local current ${parsed.type} "${parsed.path}"`
})
return JSON.stringify(serializeWorkspaceItemForRead(draft), null, 2)
return JSON.stringify(serializeWorkspaceItemForRead(currentItem), null, 2)
}
toolCallbacks.setToolStatus(toolId, {
@@ -1441,8 +1461,36 @@ export const globalTools: Tool<{}>[] = [
return deleteWorkspaceItem(parsed, ctx)
}
},
// TODO: Re-enable write_resource/write_variable tools after resource and
// variable use self-contained editor-native UserDraft values.
{
def: createToolDef(
writeResourceSchema,
'write_resource',
'Create or overwrite an AI draft resource. Does not save or deploy. Reference secret values via $var:path/to/variable; create the variable separately with write_variable.',
{ strict: false }
),
showDetails: true,
streamArguments: true,
showFade: true,
fn: async (ctx) => {
const parsed = writeResourceSchema.parse(ctx.args)
return writeResourceDraft(parsed, ctx)
}
},
{
def: createToolDef(
writeVariableSchema,
'write_variable',
'Create or overwrite an AI draft variable. Does not save or deploy. Use is_secret: true for secret values. After deploy, reference from a resource as $var:path/to/variable.',
{ strict: false }
),
showDetails: true,
streamArguments: true,
showFade: true,
fn: async (ctx) => {
const parsed = writeVariableSchema.parse(ctx.args)
return writeVariableDraft(parsed, ctx)
}
},
{
def: createToolDef(
searchResourceTypesSchema,
@@ -1616,6 +1664,8 @@ function getRequiredGlobalDraft(
function finishDraftWrite(item: WorkspaceItem, exists: boolean, ctx: WriteDraftCtx): string {
const verb = exists ? 'Updated' : 'Created'
const serializedItem =
item.type === 'variable' || item.type === 'flow' ? serializeWorkspaceItemForRead(item) : item
ctx.toolCallbacks.setToolStatus(ctx.toolId, {
content: `${verb} AI draft ${item.type} "${item.path}"`,
result: `Draft ${verb.toLowerCase()}`
@@ -1624,7 +1674,7 @@ function finishDraftWrite(item: WorkspaceItem, exists: boolean, ctx: WriteDraftC
{
success: true,
message: `${verb} AI draft ${item.type} "${item.path}". The workspace was not saved or deployed.`,
item
item: serializedItem
},
null,
2
@@ -1790,6 +1840,48 @@ async function writeTriggerDraft(
)
}
async function writeResourceDraft(
args: z.infer<typeof writeResourceSchema>,
ctx: WriteDraftCtx
): Promise<string> {
const { workspace } = ctx
startDraftWrite(ctx, 'resource', args.path)
const draft = structuredClone(args)
const existingDraft = UserDraft.get<typeof draft>('resource', args.path, { workspace })
const backendExists = existingDraft
? false
: await ResourceService.existsResource({ workspace, path: args.path })
UserDraft.save('resource', args.path, draft, { workspace })
return finishDraftWrite(
getRequiredGlobalDraft(workspace, 'resource', args.path),
existingDraft !== undefined || backendExists,
ctx
)
}
async function writeVariableDraft(
args: z.infer<typeof writeVariableSchema>,
ctx: WriteDraftCtx
): Promise<string> {
const { workspace } = ctx
startDraftWrite(ctx, 'variable', args.path)
const draft = structuredClone(args)
const existingDraft = UserDraft.get<typeof draft>('variable', args.path, { workspace })
const backendExists = existingDraft
? false
: await VariableService.existsVariable({ workspace, path: args.path })
UserDraft.save('variable', args.path, draft, { workspace })
return finishDraftWrite(
getRequiredGlobalDraft(workspace, 'variable', args.path),
existingDraft !== undefined || backendExists,
ctx
)
}
function saveAppDraft(workspace: string, path: string, value: AppDraftValue): WorkspaceItem {
UserDraft.save('raw_app', path, normalizeAppDraftValue(value), { workspace })
return getRequiredGlobalDraft(workspace, 'app', path)
@@ -1799,12 +1891,16 @@ async function loadScriptForEdit(
path: string,
workspace: string
): Promise<{ content: string; language: ScriptLang; summary?: string }> {
const draft = getGlobalDraft(workspace, 'script', path)
if (draft) {
if (typeof draft.value !== 'string' || !draft.language) {
throw new Error(`Draft script "${path}" is missing content or language.`)
const currentItem = getGlobalCurrentItem(workspace, 'script', path)
if (currentItem) {
if (typeof currentItem.value !== 'string' || !currentItem.language) {
throw new Error(`Local script "${path}" is missing content or language.`)
}
return {
content: currentItem.value,
language: currentItem.language,
summary: currentItem.summary
}
return { content: draft.value, language: draft.language, summary: draft.summary }
}
const script = await ScriptService.getScriptByPath({ workspace, path })
return { content: script.content, language: script.language, summary: script.summary }
@@ -1834,12 +1930,12 @@ async function loadFlowDraftValue(
path: string,
workspace: string
): Promise<{ flow: FlowDraftValue; summary?: string }> {
const draft = getGlobalDraft(workspace, 'flow', path)
if (draft) {
if (draft.value === undefined || typeof draft.value === 'string') {
throw new Error(`Draft flow "${path}" has no value.`)
const currentItem = getGlobalCurrentItem(workspace, 'flow', path)
if (currentItem) {
if (currentItem.value === undefined || typeof currentItem.value === 'string') {
throw new Error(`Local flow "${path}" has no value.`)
}
return { flow: draft.value as FlowDraftValue, summary: draft.summary }
return { flow: currentItem.value as FlowDraftValue, summary: currentItem.summary }
}
const flow = await FlowService.getFlowByPath({ workspace, path })
return {
@@ -1,4 +1,10 @@
import type { Flow, NewSchedule, NewScript } from '$lib/gen/types.gen'
import type {
CreateResource,
CreateVariable,
Flow,
NewSchedule,
NewScript
} from '$lib/gen/types.gen'
import { UserDraft, type UserDraftItemKind, type UserDraftListEntry } from '$lib/userDraft.svelte'
import {
getWorkspaceItemKey,
@@ -9,7 +15,14 @@ import {
type WorkspaceItemType
} from './workspaceItems'
type SharedWorkspaceItemType = 'script' | 'flow' | 'app' | 'schedule' | 'trigger'
type SharedWorkspaceItemType =
| 'script'
| 'flow'
| 'app'
| 'schedule'
| 'trigger'
| 'resource'
| 'variable'
const TRIGGER_DRAFT_KIND_BY_TRIGGER_KIND = {
http: 'trigger_http',
@@ -48,15 +61,12 @@ const SHARED_DRAFT_KINDS = [
'trigger_mqtt',
'trigger_sqs',
'trigger_gcp',
'trigger_azure'
'trigger_azure',
'resource',
'variable'
] as const satisfies UserDraftItemKind[]
const DEFAULT_APP_DATA = { tables: [], datatable: undefined, schema: undefined }
// TODO: Add resource and variable here only after their editor-native
// UserDraft contracts are self-contained. Resource drafts currently need
// resource_type outside the draft value, and new variable drafts do not have
// a complete persisted/live empty-path contract.
function clone<T>(value: T): T {
return structuredClone(value) as T
}
@@ -67,7 +77,9 @@ function isSharedWorkspaceItemType(type: WorkspaceItemType): type is SharedWorks
type === 'flow' ||
type === 'app' ||
type === 'schedule' ||
type === 'trigger'
type === 'trigger' ||
type === 'resource' ||
type === 'variable'
)
}
@@ -85,6 +97,9 @@ function sharedDraftKind(
return 'trigger_schedule'
case 'trigger':
return triggerKind ? TRIGGER_DRAFT_KIND_BY_TRIGGER_KIND[triggerKind] : undefined
case 'resource':
case 'variable':
return type
}
}
@@ -101,18 +116,18 @@ function getItemSummary(value: unknown): string | undefined {
return ((value as { summary?: string | null } | undefined)?.summary ?? undefined) || undefined
}
function scriptDraftToWorkspaceItem(path: string, draft: NewScript): WorkspaceItem {
function scriptDraftToWorkspaceItem(path: string, draft: NewScript, isDraft = true): WorkspaceItem {
return {
type: 'script',
path,
summary: draft.summary,
language: draft.language,
value: draft.content,
isDraft: true
isDraft
}
}
function flowDraftToWorkspaceItem(path: string, draft: Flow): WorkspaceItem {
function flowDraftToWorkspaceItem(path: string, draft: Flow, isDraft = true): WorkspaceItem {
return {
type: 'flow',
path,
@@ -122,35 +137,44 @@ function flowDraftToWorkspaceItem(path: string, draft: Flow): WorkspaceItem {
schema: draft.schema ?? null,
groups: draft.value.groups ?? null
},
isDraft: true
isDraft
}
}
function appDraftToWorkspaceItem(path: string, draft: AppDraftValue): WorkspaceItem {
function appDraftToWorkspaceItem(
path: string,
draft: AppDraftValue,
isDraft = true
): WorkspaceItem {
const value = normalizeAppDraftValue(draft)
return {
type: 'app',
path,
summary: value.summary,
value,
isDraft: true
isDraft
}
}
function scheduleDraftToWorkspaceItem(path: string, draft: NewSchedule): WorkspaceItem {
function scheduleDraftToWorkspaceItem(
path: string,
draft: NewSchedule,
isDraft = true
): WorkspaceItem {
return {
type: 'schedule',
path,
summary: draft.summary ?? undefined,
value: clone(draft),
isDraft: true
isDraft
}
}
function triggerDraftToWorkspaceItem(
kind: TriggerKind,
path: string,
draft: TriggerRequestBody
draft: TriggerRequestBody,
isDraft = true
): WorkspaceItem {
return {
type: 'trigger',
@@ -158,24 +182,64 @@ function triggerDraftToWorkspaceItem(
path,
summary: getItemSummary(draft),
value: clone(draft),
isDraft: true
isDraft
}
}
function sharedDraftEntryToWorkspaceItem(entry: UserDraftListEntry): WorkspaceItem | undefined {
function resourceDraftToWorkspaceItem(
path: string,
draft: CreateResource,
isDraft = true
): WorkspaceItem {
return {
type: 'resource',
path,
summary: draft.description ?? undefined,
value: clone(draft),
isDraft
}
}
function variableDraftToWorkspaceItem(
path: string,
draft: CreateVariable,
isDraft = true
): WorkspaceItem {
return {
type: 'variable',
path,
summary: draft.description ?? undefined,
value: clone(draft),
isDraft
}
}
function sharedDraftEntryToWorkspaceItem(
entry: UserDraftListEntry,
isDraft = true
): WorkspaceItem | undefined {
switch (entry.itemKind) {
case 'script':
return scriptDraftToWorkspaceItem(entry.path, entry.value as NewScript)
return scriptDraftToWorkspaceItem(entry.path, entry.value as NewScript, isDraft)
case 'flow':
return flowDraftToWorkspaceItem(entry.path, entry.value as Flow)
return flowDraftToWorkspaceItem(entry.path, entry.value as Flow, isDraft)
case 'raw_app':
return appDraftToWorkspaceItem(entry.path, entry.value as AppDraftValue)
return appDraftToWorkspaceItem(entry.path, entry.value as AppDraftValue, isDraft)
case 'trigger_schedule':
return scheduleDraftToWorkspaceItem(entry.path, entry.value as NewSchedule)
return scheduleDraftToWorkspaceItem(entry.path, entry.value as NewSchedule, isDraft)
case 'resource':
return resourceDraftToWorkspaceItem(entry.path, entry.value as CreateResource, isDraft)
case 'variable':
return variableDraftToWorkspaceItem(entry.path, entry.value as CreateVariable, isDraft)
default:
const triggerKind = TRIGGER_KIND_BY_DRAFT_KIND[entry.itemKind]
return triggerKind
? triggerDraftToWorkspaceItem(triggerKind, entry.path, entry.value as TriggerRequestBody)
? triggerDraftToWorkspaceItem(
triggerKind,
entry.path,
entry.value as TriggerRequestBody,
isDraft
)
: undefined
}
}
@@ -184,7 +248,8 @@ function getSharedDraft(
workspace: string,
type: SharedWorkspaceItemType,
path: string,
triggerKind?: TriggerKind
triggerKind?: TriggerKind,
isDraft = true
): WorkspaceItem | undefined {
const itemKind = sharedDraftKind(type, triggerKind)
if (!itemKind) return undefined
@@ -193,17 +258,21 @@ function getSharedDraft(
switch (type) {
case 'script':
return scriptDraftToWorkspaceItem(path, draft as NewScript)
return scriptDraftToWorkspaceItem(path, draft as NewScript, isDraft)
case 'flow':
return flowDraftToWorkspaceItem(path, draft as Flow)
return flowDraftToWorkspaceItem(path, draft as Flow, isDraft)
case 'app':
return appDraftToWorkspaceItem(path, draft as AppDraftValue)
return appDraftToWorkspaceItem(path, draft as AppDraftValue, isDraft)
case 'schedule':
return scheduleDraftToWorkspaceItem(path, draft as NewSchedule)
return scheduleDraftToWorkspaceItem(path, draft as NewSchedule, isDraft)
case 'trigger':
return triggerKind
? triggerDraftToWorkspaceItem(triggerKind, path, draft as TriggerRequestBody)
? triggerDraftToWorkspaceItem(triggerKind, path, draft as TriggerRequestBody, isDraft)
: undefined
case 'resource':
return resourceDraftToWorkspaceItem(path, draft as CreateResource, isDraft)
case 'variable':
return variableDraftToWorkspaceItem(path, draft as CreateVariable, isDraft)
}
}
@@ -230,6 +299,23 @@ export function getGlobalDraft(
return undefined
}
/**
* Returns the freshest frontend-visible value for AI context. This may be a
* real local draft, an AI-written UserDraft, or a clean live editor baseline.
* Therefore the returned item is intentionally not marked as `isDraft`.
*/
export function getGlobalCurrentItem(
workspace: string,
type: WorkspaceItemType,
path: string,
triggerKind?: TriggerKind
): WorkspaceItem | undefined {
if (isSharedWorkspaceItemType(type)) {
return getSharedDraft(workspace, type, path, triggerKind, false)
}
return undefined
}
export function listGlobalDrafts(workspace: string): WorkspaceItem[] {
const drafts = new Map<string, WorkspaceItem>()
@@ -242,6 +328,22 @@ export function listGlobalDrafts(workspace: string): WorkspaceItem[] {
return Array.from(drafts.values())
}
/**
* Lists local/current entries for AI context. Entries are not marked as
* `isDraft` because UserDraft.list can include clean live editor baselines.
*/
export function listGlobalCurrentItems(workspace: string): WorkspaceItem[] {
const items = new Map<string, WorkspaceItem>()
for (const entry of UserDraft.list({ workspace, itemKinds: [...SHARED_DRAFT_KINDS] })) {
const item = sharedDraftEntryToWorkspaceItem(entry, false)
if (!item) continue
items.set(getWorkspaceItemKey(item.type, item.path, item.triggerKind), item)
}
return Array.from(items.values())
}
export function deleteGlobalDraft(
workspace: string,
type: WorkspaceItemType,