refactor: simplify global ai draft items

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-05-05 17:20:27 +02:00
co-authored by Claude Opus 4.5
parent d50f8adc44
commit 1ea54ca592
4 changed files with 156 additions and 359 deletions
+24 -41
View File
@@ -22,12 +22,12 @@ Global mode should be a new AI chat mode with a small curated tool surface:
2. `list_workspace_items`
- Lists current workspace scripts, flows, and AI drafts.
- Returns metadata only: `type`, `path`, `summary`, draft status, and size/version hints.
- Returns metadata only: `type`, `path`, `summary`, `language`, and `isDraft`.
- Does not include source code or flow JSON.
3. `read_workspace_item`
- Reads one item by `type` and `path`.
- If a draft exists, returns the draft and its base metadata.
- If a draft exists, returns the draft item.
- Otherwise reads from the workspace through generated service clients.
- Initial supported types: `script`, `flow`.
@@ -38,7 +38,7 @@ Global mode should be a new AI chat mode with a small curated tool surface:
5. `modify_workspace_item`
- Creates or updates a draft overlay for an existing item.
- Requires the existing item to be read first so the draft can keep base metadata.
- Requires the existing item to be read first so the model can produce a complete replacement value.
- Does not call backend create/update APIs.
## Draft Store Shape
@@ -47,27 +47,18 @@ Create a frontend store for AI workspace drafts. Suggested file:
- `frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts`
Suggested draft model:
Suggested item/draft model:
```ts
type GlobalDraftItemType = 'script' | 'flow'
type GlobalWorkspaceItemType = 'script' | 'flow'
type GlobalDraftStatus = 'new' | 'modified'
type GlobalDraftBase = {
source: 'workspace'
version?: string | number
editedAt?: string
value: unknown
}
type GlobalDraftItem = {
type: GlobalDraftItemType
type GlobalWorkspaceItem = {
type: GlobalWorkspaceItemType
path: string
status: GlobalDraftStatus
base?: GlobalDraftBase
draft: unknown
updatedAt: string
summary?: string
value: unknown // script source code or OpenFlow value
language?: ScriptLang // required for scripts, omitted for flows
isDraft: boolean
}
```
@@ -75,8 +66,8 @@ Store operations:
1. `listDrafts()`
2. `getDraft(type, path)`
3. `setNewDraft(type, path, draft, overwrite?)`
4. `setModifiedDraft(type, path, base, draft)`
3. `setNewDraft(item, overwrite?)`
4. `setModifiedDraft(item)`
5. `deleteDraft(type, path)`
6. `clearDrafts()`
@@ -101,17 +92,17 @@ Store operations:
1. Create `global/draftStore.svelte.ts`.
2. Store drafts keyed by `${type}:${path}`.
3. Preserve base metadata for modified drafts.
3. Store drafts as simple `GlobalWorkspaceItem` values with `isDraft: true`.
4. Keep draft operations synchronous and frontend-only.
5. Add small utility functions for item keys and normalized timestamps.
5. Add small utility functions for item keys.
### Step 3: Implement Instruction Tool
1. Add `get_instructions` with a Zod schema:
- `subject: 'script' | 'flow'`
2. Return concise, practical instructions:
- script: language, schema, main function expectations, path conventions;
- flow: `OpenFlow` shape, modules, IDs, summaries, validation expectations.
- script: language, source-code value, main function expectations, path conventions;
- flow: `OpenFlow` value shape, modules, IDs, summaries, validation expectations.
3. Keep detailed examples short. This tool can grow later into a workspace skill system.
### Step 4: Implement Listing Tool
@@ -121,10 +112,8 @@ Store operations:
- `ScriptService.listScripts`
- `FlowService.listFlows`
3. Merge drafts from the draft store into the result.
4. Mark each result:
- `source: 'workspace' | 'draft' | 'workspace+draft'`
- `draftStatus?: 'new' | 'modified'`
5. Return compact metadata only.
4. Mark drafts with `isDraft: true`.
5. Return compact metadata only without `value`.
### Step 5: Implement Read Tool
@@ -133,12 +122,7 @@ Store operations:
3. If no draft exists:
- scripts: `ScriptService.getScriptByPath`
- flows: `FlowService.getFlowByPath`
4. Normalize output with:
- `type`
- `path`
- `source`
- `baseVersion`
- `value`
4. Normalize output as `{ type, path, summary, value, language, isDraft }` where `value` is script source code or the OpenFlow value.
### Step 6: Implement Write Draft Tool
@@ -148,17 +132,16 @@ Store operations:
- existing draft with same key;
- existing workspace item with same type/path.
4. If conflict exists and `overwrite` is not true, return a clear error.
5. Store a `status: 'new'` draft.
5. Store the draft as a `GlobalWorkspaceItem` with `isDraft: true`.
6. Return a short summary that explicitly says the workspace was not saved.
### Step 7: Implement Modify Draft Tool
1. Add `modify_workspace_item`.
2. Require existing draft or existing workspace item.
3. If no draft exists, fetch workspace item and store it as `base`.
4. Store a `status: 'modified'` draft with the new draft value.
5. Return base version/hash metadata and a clear "draft only" message.
6. Consider requiring `baseVersion` in a follow-up after the POC to reduce stale edits.
3. Store the modified item as a draft with `isDraft: true`.
4. Return a clear "draft only" message.
5. Consider adding base version/hash checks in a follow-up after the POC to reduce stale edits.
### Step 8: Wire UI Entry Point
@@ -70,7 +70,7 @@
case AIMode.API:
return 'Make API calls...'
case AIMode.GLOBAL:
return 'Work across workspace items...'
return 'Work across scripts and flows...'
case AIMode.ASK:
return 'Ask questions about Windmill...'
default:
@@ -1,6 +1,6 @@
import { FlowService, ScriptService } from '$lib/gen'
import { $ScriptLang, FlowService, ScriptService } from '$lib/gen'
import { getFlowPrompt, getScriptPrompt } from '$system_prompts'
import type { Flow, Script } from '$lib/gen/types.gen'
import type { Flow, Script, ScriptLang } from '$lib/gen/types.gen'
import type {
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam
@@ -10,72 +10,20 @@ import { createToolDef, type Tool } from '../shared'
import {
globalDraftStore,
getGlobalDraftKey,
type GlobalDraftBase,
type GlobalDraftItem,
type GlobalWorkspaceItem,
type GlobalWorkspaceItemType
} from './draftStore.svelte'
const ITEM_TYPES = ['script', 'flow'] as const satisfies readonly GlobalWorkspaceItemType[]
const MAX_LIST_LIMIT = 100
const SCRIPT_INSTRUCTION_LANGUAGES = [
'bunnative',
'nativets',
'bun',
'deno',
'python3',
'php',
'rust',
'go',
'bash',
'postgresql',
'mysql',
'bigquery',
'snowflake',
'mssql',
'graphql',
'powershell',
'csharp',
'java',
'duckdb',
'rlang',
'oracledb',
'ansible',
'nu',
'ruby'
] as const
const SYSTEM_PROMPT_SCRIPT_LANGUAGES = new Set<string>([
'bunnative',
'nativets',
'bun',
'deno',
'python3',
'php',
'rust',
'go',
'bash',
'postgresql',
'mysql',
'bigquery',
'snowflake',
'mssql',
'graphql',
'powershell',
'csharp',
'java',
'duckdb',
'rlang'
])
const DEFAULT_SCRIPT_LANGUAGE: ScriptLang = 'bun'
const itemTypeSchema = z.enum(ITEM_TYPES)
const scriptInstructionLanguageSchema = z.enum(SCRIPT_INSTRUCTION_LANGUAGES)
type ScriptInstructionLanguage = z.infer<typeof scriptInstructionLanguageSchema>
const DEFAULT_SCRIPT_LANGUAGE: ScriptInstructionLanguage = 'bun'
const scriptLanguageSchema = z.enum($ScriptLang.enum)
const getInstructionsSchema = z.object({
subject: itemTypeSchema.describe('The workspace item type to get authoring instructions for.'),
language: scriptInstructionLanguageSchema
language: scriptLanguageSchema
.optional()
.describe(
'Required when subject is script. Use the existing script language when modifying, or the requested target language when creating.'
@@ -106,44 +54,45 @@ const readWorkspaceItemSchema = z.object({
path: z.string().describe('Workspace path of the item to read.')
})
const writeWorkspaceItemSchema = z.object({
const workspaceItemDraftBaseSchema = z.object({
type: itemTypeSchema,
path: z.string().describe('Workspace path for the new draft item.'),
draft: z.any().describe('The complete draft item payload for the item type.'),
overwrite: z
.boolean()
path: z.string().describe('Workspace path of the item.'),
summary: z.string().optional().describe('Short summary of the item.'),
value: z
.any()
.describe('For scripts, the complete script source code. For flows, the complete OpenFlow value.'),
language: scriptLanguageSchema
.optional()
.describe('If true, replace an existing AI draft with the same type and path.')
.describe('Required for script items. Omit for flow items.')
})
const modifyWorkspaceItemSchema = z.object({
type: itemTypeSchema,
path: z.string().describe('Workspace path of the existing item or AI draft to modify.'),
draft: z.any().describe('The complete modified draft item payload for the item type.')
})
type WorkspaceItemDraftInput = z.infer<typeof workspaceItemDraftBaseSchema>
type WorkspaceItemRead = {
type: GlobalWorkspaceItemType
path: string
summary?: string
version?: string | number
editedAt?: string
value: unknown
function validateWorkspaceItemDraft(
item: WorkspaceItemDraftInput,
ctx: z.RefinementCtx
): void {
if (item.type === 'script' && !item.language) {
ctx.addIssue({
code: 'custom',
path: ['language'],
message: 'language is required for script items'
})
}
}
type WorkspaceItemMetadata = {
type: GlobalWorkspaceItemType
path: string
source: 'workspace' | 'draft' | 'workspace+draft'
summary?: string
version?: string | number
editedAt?: string
draftStatus?: GlobalDraftItem['status']
size?: number
language?: string
kind?: string
moduleCount?: number
}
const writeWorkspaceItemSchema = workspaceItemDraftBaseSchema
.extend({
overwrite: z
.boolean()
.optional()
.describe('If true, replace an existing AI draft with the same type and path.')
})
.superRefine(validateWorkspaceItemDraft)
const modifyWorkspaceItemSchema = workspaceItemDraftBaseSchema.superRefine(validateWorkspaceItemDraft)
type WorkspaceItemMetadata = Omit<GlobalWorkspaceItem, 'value'>
const GLOBAL_SYSTEM_PROMPT = `You are Windmill's global workspace assistant.
@@ -151,6 +100,10 @@ You can inspect workspace scripts and flows, then create draft changes in the fr
Important rules:
- Writes and modifications are drafts only. They do not save, deploy, or mutate backend workspace items.
- A workspace item is { type, path, summary, value, language, isDraft }.
- For scripts, value is the complete script source code and language is required.
- For flows, value is the complete OpenFlow value and language is omitted.
- list_workspace_items returns metadata only and omits value. Use read_workspace_item for the current value.
- Use list_workspace_items before broad reads.
- Use read_workspace_item before modifying an existing item unless the user already provided the complete current item.
- Use get_instructions before writing or modifying a script or flow. For scripts, pass the target language; when modifying, use the language from the item you read.
@@ -163,24 +116,6 @@ function getRequestedTypes(
return types && types.length > 0 ? types : [...ITEM_TYPES]
}
function getJsonSize(value: unknown): number | undefined {
try {
return JSON.stringify(value)?.length
} catch {
return undefined
}
}
function getObjectString(value: unknown, key: string): string | undefined {
if (!value || typeof value !== 'object') return undefined
const candidate = (value as Record<string, unknown>)[key]
return typeof candidate === 'string' ? candidate : undefined
}
function getDraftSummary(draft: GlobalDraftItem): string | undefined {
return getObjectString(draft.draft, 'summary') ?? getObjectString(draft.base?.value, 'summary')
}
function itemMatches(
item: Pick<WorkspaceItemMetadata, 'path' | 'summary'>,
query: string | undefined,
@@ -201,83 +136,44 @@ function itemMatches(
)
}
function scriptToMetadata(script: Script): WorkspaceItemMetadata {
function scriptToWorkspaceItem(script: Script): GlobalWorkspaceItem {
return {
type: 'script',
path: script.path,
source: 'workspace',
summary: script.summary,
version: script.hash,
size: script.content?.length,
value: script.content,
language: script.language,
kind: script.kind
isDraft: false
}
}
function flowToMetadata(flow: Flow): WorkspaceItemMetadata {
return {
type: 'flow',
path: flow.path,
source: 'workspace',
summary: flow.summary,
version: flow.version_id,
editedAt: flow.edited_at,
moduleCount: flow.value?.modules?.length ?? 0
}
}
function draftToMetadata(draft: GlobalDraftItem, hasWorkspaceItem: boolean): WorkspaceItemMetadata {
return {
type: draft.type,
path: draft.path,
source: hasWorkspaceItem || draft.status === 'modified' ? 'workspace+draft' : 'draft',
summary: getDraftSummary(draft),
draftStatus: draft.status,
editedAt: draft.updatedAt,
size: getJsonSize(draft.draft)
}
}
function scriptToRead(script: Script): WorkspaceItemRead {
return {
type: 'script',
path: script.path,
summary: script.summary,
version: script.hash,
value: {
path: script.path,
summary: script.summary,
description: script.description,
language: script.language,
content: script.content,
schema: script.schema,
kind: script.kind,
tag: script.tag,
lock: script.lock,
modules: script.modules,
envs: script.envs
}
}
}
function flowToRead(flow: Flow): WorkspaceItemRead {
function flowToWorkspaceItem(flow: Flow): GlobalWorkspaceItem {
return {
type: 'flow',
path: flow.path,
summary: flow.summary,
version: flow.version_id,
editedAt: flow.edited_at,
value: {
path: flow.path,
summary: flow.summary,
description: flow.description,
schema: flow.schema,
value: flow.value,
tag: flow.tag,
priority: flow.priority,
dedicated_worker: flow.dedicated_worker,
timeout: flow.timeout
}
value: flow.value,
isDraft: false
}
}
function toMetadata(item: GlobalWorkspaceItem): WorkspaceItemMetadata {
return {
type: item.type,
path: item.path,
summary: item.summary,
language: item.language,
isDraft: item.isDraft
}
}
function toDraftInput(item: WorkspaceItemDraftInput): Omit<GlobalWorkspaceItem, 'isDraft'> {
return {
type: item.type,
path: item.path,
summary: item.summary,
value: item.value,
language: item.type === 'script' ? item.language : undefined
}
}
@@ -298,12 +194,12 @@ async function readWorkspaceItem(
type: GlobalWorkspaceItemType,
path: string,
workspace: string
): Promise<WorkspaceItemRead> {
): Promise<GlobalWorkspaceItem> {
switch (type) {
case 'script':
return scriptToRead(await ScriptService.getScriptByPath({ workspace, path }))
return scriptToWorkspaceItem(await ScriptService.getScriptByPath({ workspace, path }))
case 'flow':
return flowToRead(await FlowService.getFlowByPath({ workspace, path }))
return flowToWorkspaceItem(await FlowService.getFlowByPath({ workspace, path }))
}
}
@@ -324,7 +220,7 @@ async function listWorkspaceMetadata(
withoutDescription: true
})
for (const script of scripts) {
items.set(getGlobalDraftKey('script', script.path), scriptToMetadata(script))
items.set(getGlobalDraftKey('script', script.path), toMetadata(scriptToWorkspaceItem(script)))
}
}
@@ -337,30 +233,25 @@ async function listWorkspaceMetadata(
withoutDescription: true
})
for (const flow of flows) {
items.set(getGlobalDraftKey('flow', flow.path), flowToMetadata(flow))
items.set(getGlobalDraftKey('flow', flow.path), toMetadata(flowToWorkspaceItem(flow)))
}
}
return items
}
function getScriptInstructions(language: ScriptInstructionLanguage | undefined): string {
function getScriptInstructions(language: ScriptLang | undefined): string {
const selectedLanguage = language ?? DEFAULT_SCRIPT_LANGUAGE
const languageAvailabilityNote = SYSTEM_PROMPT_SCRIPT_LANGUAGES.has(selectedLanguage)
? `Using the shared system_prompts script guidance for \`${selectedLanguage}\`.`
: `No dedicated system_prompts language skill exists yet for \`${selectedLanguage}\`; preserve the existing runtime conventions and use the generic Windmill script guidance below.`
const defaultLanguageNote = language
? ''
: `\n- No script language was provided. Default to \`${DEFAULT_SCRIPT_LANGUAGE}\` only for new TypeScript scripts; if the user requested another language or you read an existing script, call get_instructions again with that language.`
return `# Global draft script instructions
${languageAvailabilityNote}
- Global mode writes complete draft payloads only; it does not save, deploy, run, or generate metadata.
- Draft payloads for scripts should be NewScript-like objects with: \`path\`, \`summary\`, optional \`description\`, \`language\`, \`content\`, optional JSON Schema \`schema\`, optional \`kind\`, \`tag\`, \`envs\`, \`lock\`, and \`modules\` when needed.
- Use workspace paths such as \`f/folder/name\` or \`u/username/name\`. Preserve the current path/language/kind when modifying unless the user asked to change them.
- For normal scripts, \`kind\` should be \`script\`; use \`preprocessor\`, \`failure\`, etc. only when that is the actual script role.
- Draft payloads for scripts are workspace items: \`{ type: "script", path, summary, value, language }\`.
- For scripts, \`value\` is the complete script source code and \`language\` is required.
- Use workspace paths such as \`f/folder/name\` or \`u/username/name\`. Preserve the current path/language when modifying unless the user asked to change them.
- Return the full desired script draft to \`write_workspace_item\` or \`modify_workspace_item\`, not a patch or partial object.${defaultLanguageNote}
# Windmill script authoring reference (${selectedLanguage})
@@ -372,7 +263,8 @@ function getFlowInstructions(): string {
return `# Global draft flow instructions
- Global mode writes complete draft payloads only; it does not save, deploy, run, scaffold local files, or generate metadata.
- Draft payloads for flows should be Flow-like objects with: \`path\`, \`summary\`, optional \`description\`, optional input \`schema\`, and \`value\` containing the OpenFlow value.
- Draft payloads for flows are workspace items: \`{ type: "flow", path, summary, value }\`.
- For flows, \`value\` is the complete OpenFlow value. Omit \`language\`.
- \`value.modules\` contains normal sequential modules. Use top-level \`value.preprocessor_module\` and \`value.failure_module\` for special modules; do not put \`preprocessor\` or \`failure\` in \`value.modules\`.
- Every module needs a stable unique \`id\` and a useful \`summary\` when the schema supports it.
- Prefer path/script/flow modules when composing existing workspace logic. Use rawscript modules only when new inline code is needed.
@@ -386,7 +278,7 @@ ${getFlowPrompt()}`
function getInstructions(
subject: GlobalWorkspaceItemType,
language?: ScriptInstructionLanguage
language?: ScriptLang
): string {
switch (subject) {
case 'script':
@@ -438,12 +330,10 @@ export const globalTools: Tool<{}>[] = [
if (!types.includes(draft.type)) continue
const key = getGlobalDraftKey(draft.type, draft.path)
const existing = workspaceItems.get(key)
const draftMetadata = draftToMetadata(draft, existing !== undefined)
const draftMetadata = toMetadata(draft)
workspaceItems.set(key, {
...existing,
...draftMetadata,
summary: draftMetadata.summary ?? existing?.summary,
version: existing?.version ?? draft.base?.version
summary: draftMetadata.summary ?? existing?.summary
})
}
@@ -470,19 +360,7 @@ export const globalTools: Tool<{}>[] = [
toolCallbacks.setToolStatus(toolId, {
content: `Read AI draft ${parsedArgs.type} "${parsedArgs.path}"`
})
return JSON.stringify(
{
type: draft.type,
path: draft.path,
source: draft.status === 'new' ? 'draft' : 'workspace+draft',
draftStatus: draft.status,
baseVersion: draft.base?.version,
baseEditedAt: draft.base?.editedAt,
draft: draft.draft
},
null,
2
)
return JSON.stringify(draft, null, 2)
}
toolCallbacks.setToolStatus(toolId, {
@@ -492,45 +370,41 @@ export const globalTools: Tool<{}>[] = [
toolCallbacks.setToolStatus(toolId, {
content: `Read ${parsedArgs.type} "${parsedArgs.path}"`
})
return JSON.stringify({ ...item, source: 'workspace' }, null, 2)
return JSON.stringify(item, null, 2)
}
},
{
def: createToolDef(
writeWorkspaceItemSchema,
'write_workspace_item',
'Create a new AI draft item. This does not save or deploy anything to the workspace.'
'Create a new AI draft workspace item. This does not save or deploy anything to the workspace.'
),
showDetails: true,
streamArguments: true,
showFade: true,
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
const parsedArgs = writeWorkspaceItemSchema.parse(args)
const draftInput = toDraftInput(parsedArgs)
toolCallbacks.setToolStatus(toolId, {
content: `Creating draft ${parsedArgs.type} "${parsedArgs.path}"...`
content: `Creating draft ${draftInput.type} "${draftInput.path}"...`
})
if (await workspaceItemExists(parsedArgs.type, parsedArgs.path, workspace)) {
const message = `${parsedArgs.type} "${parsedArgs.path}" already exists in the workspace. Use modify_workspace_item to create a draft overlay.`
if (await workspaceItemExists(draftInput.type, draftInput.path, workspace)) {
const message = `${draftInput.type} "${draftInput.path}" already exists in the workspace. Use modify_workspace_item to create a draft overlay.`
toolCallbacks.setToolStatus(toolId, { content: message, error: message })
return JSON.stringify({ success: false, error: message })
}
try {
const draft = globalDraftStore.setNewDraft(
parsedArgs.type,
parsedArgs.path,
parsedArgs.draft,
parsedArgs.overwrite ?? false
)
const draft = globalDraftStore.setNewDraft(draftInput, parsedArgs.overwrite ?? false)
toolCallbacks.setToolStatus(toolId, {
content: `Created AI draft ${parsedArgs.type} "${parsedArgs.path}"`,
content: `Created AI draft ${draft.type} "${draft.path}"`,
result: 'Draft created'
})
return JSON.stringify(
{
success: true,
message: `Created AI draft ${parsedArgs.type} "${parsedArgs.path}". The workspace was not saved or deployed.`,
message: `Created AI draft ${draft.type} "${draft.path}". The workspace was not saved or deployed.`,
draft
},
null,
@@ -554,65 +428,27 @@ export const globalTools: Tool<{}>[] = [
showFade: true,
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
const parsedArgs = modifyWorkspaceItemSchema.parse(args)
const draftInput = toDraftInput(parsedArgs)
toolCallbacks.setToolStatus(toolId, {
content: `Updating draft ${parsedArgs.type} "${parsedArgs.path}"...`
content: `Updating draft ${draftInput.type} "${draftInput.path}"...`
})
const existingDraft = globalDraftStore.getDraft(parsedArgs.type, parsedArgs.path)
if (existingDraft?.status === 'new') {
const draft = globalDraftStore.setNewDraft(
parsedArgs.type,
parsedArgs.path,
parsedArgs.draft,
true
)
toolCallbacks.setToolStatus(toolId, {
content: `Updated AI draft ${parsedArgs.type} "${parsedArgs.path}"`,
result: 'Draft updated'
})
return JSON.stringify(
{
success: true,
message: `Updated new AI draft ${parsedArgs.type} "${parsedArgs.path}". The workspace was not saved or deployed.`,
draft
},
null,
2
)
const existingDraft = globalDraftStore.getDraft(draftInput.type, draftInput.path)
if (!existingDraft && !(await workspaceItemExists(draftInput.type, draftInput.path, workspace))) {
const message = `${draftInput.type} "${draftInput.path}" does not exist in the workspace or AI draft store. Use write_workspace_item for new items.`
toolCallbacks.setToolStatus(toolId, { content: message, error: message })
return JSON.stringify({ success: false, error: message })
}
let base: GlobalDraftBase | undefined = existingDraft?.base
if (!base) {
if (!(await workspaceItemExists(parsedArgs.type, parsedArgs.path, workspace))) {
const message = `${parsedArgs.type} "${parsedArgs.path}" does not exist in the workspace or AI draft store. Use write_workspace_item for new items.`
toolCallbacks.setToolStatus(toolId, { content: message, error: message })
return JSON.stringify({ success: false, error: message })
}
const currentItem = await readWorkspaceItem(parsedArgs.type, parsedArgs.path, workspace)
base = {
source: 'workspace',
version: currentItem.version,
editedAt: currentItem.editedAt,
value: currentItem.value
}
}
const draft = globalDraftStore.setModifiedDraft(
parsedArgs.type,
parsedArgs.path,
base,
parsedArgs.draft
)
const draft = globalDraftStore.setModifiedDraft(draftInput)
toolCallbacks.setToolStatus(toolId, {
content: `Updated AI draft ${parsedArgs.type} "${parsedArgs.path}"`,
content: `Updated AI draft ${draft.type} "${draft.path}"`,
result: 'Draft updated'
})
return JSON.stringify(
{
success: true,
message: `Updated AI draft ${parsedArgs.type} "${parsedArgs.path}". The workspace was not saved or deployed.`,
baseVersion: base.version,
baseEditedAt: base.editedAt,
message: `Updated AI draft ${draft.type} "${draft.path}". The workspace was not saved or deployed.`,
draft
},
null,
@@ -1,21 +1,14 @@
import type { ScriptLang } from '$lib/gen/types.gen'
export type GlobalWorkspaceItemType = 'script' | 'flow'
export type GlobalDraftStatus = 'new' | 'modified'
export type GlobalDraftBase = {
source: 'workspace'
version?: string | number
editedAt?: string
value: unknown
}
export type GlobalDraftItem = {
export type GlobalWorkspaceItem = {
type: GlobalWorkspaceItemType
path: string
status: GlobalDraftStatus
base?: GlobalDraftBase
draft: unknown
updatedAt: string
summary?: string
value: unknown
language?: ScriptLang
isDraft: boolean
}
export function getGlobalDraftKey(type: GlobalWorkspaceItemType, path: string): string {
@@ -26,56 +19,41 @@ function cloneValue<T>(value: T): T {
return structuredClone($state.snapshot(value)) as T
}
class GlobalDraftStore {
private drafts = $state<Record<string, GlobalDraftItem>>({})
type DraftInput = Omit<GlobalWorkspaceItem, 'isDraft'>
listDrafts(): GlobalDraftItem[] {
function toDraftItem(input: DraftInput): GlobalWorkspaceItem {
return {
...cloneValue(input),
isDraft: true
}
}
class GlobalDraftStore {
private drafts = $state<Record<string, GlobalWorkspaceItem>>({})
listDrafts(): GlobalWorkspaceItem[] {
return Object.values(this.drafts).map((draft) => cloneValue(draft))
}
getDraft(type: GlobalWorkspaceItemType, path: string): GlobalDraftItem | undefined {
getDraft(type: GlobalWorkspaceItemType, path: string): GlobalWorkspaceItem | undefined {
const draft = this.drafts[getGlobalDraftKey(type, path)]
return draft ? cloneValue(draft) : undefined
}
setNewDraft(
type: GlobalWorkspaceItemType,
path: string,
draft: unknown,
overwrite = false
): GlobalDraftItem {
const key = getGlobalDraftKey(type, path)
setNewDraft(input: DraftInput, overwrite = false): GlobalWorkspaceItem {
const key = getGlobalDraftKey(input.type, input.path)
if (this.drafts[key] && !overwrite) {
throw new Error(`A draft already exists for ${type} "${path}".`)
throw new Error(`A draft already exists for ${input.type} "${input.path}".`)
}
const item: GlobalDraftItem = {
type,
path,
status: 'new',
draft: cloneValue(draft),
updatedAt: new Date().toISOString()
}
const item = toDraftItem(input)
this.drafts[key] = item
return cloneValue(item)
}
setModifiedDraft(
type: GlobalWorkspaceItemType,
path: string,
base: GlobalDraftBase,
draft: unknown
): GlobalDraftItem {
const key = getGlobalDraftKey(type, path)
const item: GlobalDraftItem = {
type,
path,
status: 'modified',
base: cloneValue(base),
draft: cloneValue(draft),
updatedAt: new Date().toISOString()
}
this.drafts[key] = item
setModifiedDraft(input: DraftInput): GlobalWorkspaceItem {
const item = toDraftItem(input)
this.drafts[getGlobalDraftKey(input.type, input.path)] = item
return cloneValue(item)
}
@@ -87,11 +65,11 @@ class GlobalDraftStore {
this.drafts = {}
}
getScriptDraft(path: string): GlobalDraftItem | undefined {
getScriptDraft(path: string): GlobalWorkspaceItem | undefined {
return this.getDraft('script', path)
}
getFlowDraft(path: string): GlobalDraftItem | undefined {
getFlowDraft(path: string): GlobalWorkspaceItem | undefined {
return this.getDraft('flow', path)
}
}