From 10d1fde74fa65af20dd646e91573195103114580 Mon Sep 17 00:00:00 2001 From: centdix Date: Tue, 19 May 2026 17:23:16 +0200 Subject: [PATCH] fix: resolve add-editor drafts by path --- .../copilot/chat/global/core.test.ts | 40 ++++++ .../components/copilot/chat/global/core.ts | 21 ++-- .../copilot/chat/global/userDraftAdapter.ts | 119 ++++++++++++++---- 3 files changed, 147 insertions(+), 33 deletions(-) diff --git a/frontend/src/lib/components/copilot/chat/global/core.test.ts b/frontend/src/lib/components/copilot/chat/global/core.test.ts index f78328f066..3055a1cb8c 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.test.ts @@ -257,6 +257,46 @@ describe('global AI tools', () => { expect(localStorage.getItem(`userdraft/w/${WORKSPACE}/script/f/scripts/open-editor`)).toBeNull() }) + it('lists and edits a new script draft stored under the empty add-editor key by its assigned path', async () => { + const content = 'export async function main() {\n\treturn 1\n}' + const handle = UserDraft.use('script', '', { workspace: WORKSPACE }) + handle.setDraftAndMeta( + { + path: 'u/admin/assigned_script', + summary: '', + language: 'bun', + content + }, + {} + ) + + const raw = await callGlobalTool('list_workspace_items', { + types: ['script'] + }) + + expect(JSON.parse(raw)).toEqual([ + expect.objectContaining({ + type: 'script', + path: 'u/admin/assigned_script', + isDraft: false + }) + ]) + + await callGlobalTool('edit_script', { + path: 'u/admin/assigned_script', + old_string: 'return 1', + new_string: 'return 2', + replace_all: false + }) + + expect(UserDraft.get('script', '', { workspace: WORKSPACE })?.content).toContain( + 'return 2' + ) + expect( + UserDraft.get('script', 'u/admin/assigned_script', { workspace: WORKSPACE }) + ).toBeUndefined() + }) + it('overlays shared UserDraft entries in list_workspace_items', async () => { await callGlobalTool('write_script', { path: 'f/scripts/listed', diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index c412279a6d..0b791de01a 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -81,6 +81,7 @@ import { deleteGlobalDraft, getGlobalCurrentItem, getGlobalDraft, + getGlobalDraftStoragePath, listGlobalCurrentItems, triggerKindToUserDraftKind } from './userDraftAdapter' @@ -659,11 +660,11 @@ function buildPersistedRunnable( ): PersistedRunnable { const fields = input.staticInputs ? Object.fromEntries( - Object.entries(input.staticInputs).map(([k, v]) => [ - k, - { type: 'static', value: v, fieldType: 'object' } - ]) - ) + Object.entries(input.staticInputs).map(([k, v]) => [ + k, + { type: 'static', value: v, fieldType: 'object' } + ]) + ) : (existing?.fields ?? {}) if (input.type === 'inline') { @@ -1696,7 +1697,8 @@ async function writeScriptDraft( const { workspace } = ctx startDraftWrite(ctx, 'script', args.path) - const existingDraft = UserDraft.get('script', args.path, { workspace }) + const draftStoragePath = getGlobalDraftStoragePath(workspace, 'script', args.path) ?? args.path + const existingDraft = UserDraft.get('script', draftStoragePath, { workspace }) const backendExists = existingDraft ? false : await ScriptService.existsScriptByPath({ workspace, path: args.path }) @@ -1733,7 +1735,7 @@ async function writeScriptDraft( } } - UserDraft.save('script', args.path, draft, { workspace }) + UserDraft.save('script', draftStoragePath, draft, { workspace }) return finishDraftWrite( getRequiredGlobalDraft(workspace, 'script', args.path), existingDraft !== undefined || backendExists, @@ -1748,13 +1750,14 @@ async function writeFlowDraft( const { workspace } = ctx startDraftWrite(ctx, 'flow', args.path) + const draftStoragePath = getGlobalDraftStoragePath(workspace, 'flow', args.path) ?? args.path const draftValue = args.flow const value = structuredClone(draftValue.value) if (draftValue.groups !== undefined && draftValue.groups !== null) { value.groups = structuredClone(draftValue.groups) } - const existingDraft = UserDraft.get('flow', args.path, { workspace }) + const existingDraft = UserDraft.get('flow', draftStoragePath, { workspace }) const backendExists = existingDraft ? false : await FlowService.existsFlowByPath({ workspace, path: args.path }) @@ -1790,7 +1793,7 @@ async function writeFlowDraft( } } - UserDraft.save('flow', args.path, draft, { workspace }) + UserDraft.save('flow', draftStoragePath, draft, { workspace }) return finishDraftWrite( getRequiredGlobalDraft(workspace, 'flow', args.path), existingDraft !== undefined || backendExists, diff --git a/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts b/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts index 716bc54a7a..6bc0621203 100644 --- a/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts +++ b/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts @@ -149,7 +149,19 @@ function getItemSummary(value: unknown): string | undefined { return ((value as { summary?: string | null } | undefined)?.summary ?? undefined) || undefined } -function scriptDraftToWorkspaceItem(path: string, draft: NewScript, isDraft = true): WorkspaceItem { +function getItemPath(storagePath: string, value: unknown): string | undefined { + const valuePath = (value as { path?: string | null } | undefined)?.path + return valuePath?.trim() || storagePath || undefined +} + +function scriptDraftToWorkspaceItem( + storagePath: string, + draft: NewScript, + isDraft = true +): WorkspaceItem | undefined { + const path = getItemPath(storagePath, draft) + if (!path) return undefined + return { type: 'script', path, @@ -174,7 +186,7 @@ function getFlowSchema(value: unknown): Record | null { } function flowDraftToWorkspaceItem( - path: string, + storagePath: string, draft: unknown, isDraft = true ): WorkspaceItem | undefined { @@ -182,6 +194,8 @@ function flowDraftToWorkspaceItem( const value = getFlowValue(draft.value) ?? getFlowValue(draft) if (!value) return undefined + const path = getItemPath(storagePath, draft) + if (!path) return undefined return { type: 'flow', @@ -199,11 +213,14 @@ function flowDraftToWorkspaceItem( } function appDraftToWorkspaceItem( - path: string, + storagePath: string, draft: AppDraftValue, isDraft = true -): WorkspaceItem { +): WorkspaceItem | undefined { const value = normalizeAppDraftValue(draft) + const path = getItemPath(storagePath, value) + if (!path) return undefined + return { type: 'app', path, @@ -214,10 +231,13 @@ function appDraftToWorkspaceItem( } function scheduleDraftToWorkspaceItem( - path: string, + storagePath: string, draft: NewSchedule, isDraft = true -): WorkspaceItem { +): WorkspaceItem | undefined { + const path = getItemPath(storagePath, draft) + if (!path) return undefined + return { type: 'schedule', path, @@ -229,10 +249,13 @@ function scheduleDraftToWorkspaceItem( function triggerDraftToWorkspaceItem( kind: TriggerKind, - path: string, + storagePath: string, draft: TriggerRequestBody, isDraft = true -): WorkspaceItem { +): WorkspaceItem | undefined { + const path = getItemPath(storagePath, draft) + if (!path) return undefined + return { type: 'trigger', triggerKind: kind, @@ -244,10 +267,13 @@ function triggerDraftToWorkspaceItem( } function resourceDraftToWorkspaceItem( - path: string, + storagePath: string, draft: CreateResource, isDraft = true -): WorkspaceItem { +): WorkspaceItem | undefined { + const path = getItemPath(storagePath, draft) + if (!path) return undefined + return { type: 'resource', path, @@ -258,10 +284,13 @@ function resourceDraftToWorkspaceItem( } function variableDraftToWorkspaceItem( - path: string, + storagePath: string, draft: CreateVariable, isDraft = true -): WorkspaceItem { +): WorkspaceItem | undefined { + const path = getItemPath(storagePath, draft) + if (!path) return undefined + return { type: 'variable', path, @@ -301,6 +330,32 @@ function sharedDraftEntryToWorkspaceItem( } } +type SharedDraftLookup = { + storagePath: string + value: unknown +} + +function findSharedDraft( + workspace: string, + type: SharedWorkspaceItemType, + path: string, + triggerKind?: TriggerKind +): SharedDraftLookup | undefined { + const itemKind = sharedDraftKind(type, triggerKind) + if (!itemKind) return undefined + + const direct = UserDraft.get(itemKind, path, { workspace }) + if (direct !== undefined) return { storagePath: path, value: direct } + + for (const entry of UserDraft.list({ workspace, itemKinds: [itemKind] })) { + if (getItemPath(entry.path, entry.value) === path) { + return { storagePath: entry.path, value: entry.value } + } + } + + return undefined +} + function getSharedDraft( workspace: string, type: SharedWorkspaceItemType, @@ -308,28 +363,31 @@ function getSharedDraft( triggerKind?: TriggerKind, isDraft = true ): WorkspaceItem | undefined { - const itemKind = sharedDraftKind(type, triggerKind) - if (!itemKind) return undefined - const draft = UserDraft.get(itemKind, path, { workspace }) - if (draft === undefined) return undefined + const draft = findSharedDraft(workspace, type, path, triggerKind) + if (!draft) return undefined switch (type) { case 'script': - return scriptDraftToWorkspaceItem(path, draft as NewScript, isDraft) + return scriptDraftToWorkspaceItem(draft.storagePath, draft.value as NewScript, isDraft) case 'flow': - return flowDraftToWorkspaceItem(path, draft, isDraft) + return flowDraftToWorkspaceItem(draft.storagePath, draft.value, isDraft) case 'app': - return appDraftToWorkspaceItem(path, draft as AppDraftValue, isDraft) + return appDraftToWorkspaceItem(draft.storagePath, draft.value as AppDraftValue, isDraft) case 'schedule': - return scheduleDraftToWorkspaceItem(path, draft as NewSchedule, isDraft) + return scheduleDraftToWorkspaceItem(draft.storagePath, draft.value as NewSchedule, isDraft) case 'trigger': return triggerKind - ? triggerDraftToWorkspaceItem(triggerKind, path, draft as TriggerRequestBody, isDraft) + ? triggerDraftToWorkspaceItem( + triggerKind, + draft.storagePath, + draft.value as TriggerRequestBody, + isDraft + ) : undefined case 'resource': - return resourceDraftToWorkspaceItem(path, draft as CreateResource, isDraft) + return resourceDraftToWorkspaceItem(draft.storagePath, draft.value as CreateResource, isDraft) case 'variable': - return variableDraftToWorkspaceItem(path, draft as CreateVariable, isDraft) + return variableDraftToWorkspaceItem(draft.storagePath, draft.value as CreateVariable, isDraft) } } @@ -341,7 +399,8 @@ function deleteSharedDraft( ): void { const itemKind = sharedDraftKind(type, triggerKind) if (!itemKind) return - UserDraft.remove(itemKind, path, { workspace }) + const draft = findSharedDraft(workspace, type, path, triggerKind) + UserDraft.remove(itemKind, draft?.storagePath ?? path, { workspace }) } export function getGlobalDraft( @@ -373,6 +432,18 @@ export function getGlobalCurrentItem( return undefined } +export function getGlobalDraftStoragePath( + workspace: string, + type: WorkspaceItemType, + path: string, + triggerKind?: TriggerKind +): string | undefined { + if (isSharedWorkspaceItemType(type)) { + return findSharedDraft(workspace, type, path, triggerKind)?.storagePath + } + return undefined +} + export function listGlobalDrafts(workspace: string): WorkspaceItem[] { const drafts = new Map()