diff --git a/PLAN.md b/PLAN.md index 16ce34b9d1..88744a783e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 diff --git a/frontend/src/lib/components/copilot/chat/AIChatInput.svelte b/frontend/src/lib/components/copilot/chat/AIChatInput.svelte index 833649e9b6..a08c0ab91e 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatInput.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChatInput.svelte @@ -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: diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index dfd8e85bf9..d3efd429ee 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -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([ - '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 -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 -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 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)[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, 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 { + 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 { +): Promise { 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, diff --git a/frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts b/frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts index 7b0c712cf8..60d22bc3cf 100644 --- a/frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts @@ -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(value: T): T { return structuredClone($state.snapshot(value)) as T } -class GlobalDraftStore { - private drafts = $state>({}) +type DraftInput = Omit - listDrafts(): GlobalDraftItem[] { +function toDraftItem(input: DraftInput): GlobalWorkspaceItem { + return { + ...cloneValue(input), + isDraft: true + } +} + +class GlobalDraftStore { + private drafts = $state>({}) + + 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) } }