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 b7a53f5604..24703794d8 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.test.ts @@ -361,6 +361,87 @@ describe('global AI tools', () => { ) }) + it('reads saved DB script drafts when no local draft exists', async () => { + vi.mocked(ScriptService.getScriptByPathWithDraft).mockResolvedValueOnce({ + path: 'f/scripts/existing', + hash: 'deployed-hash', + summary: 'deployed summary', + description: 'deployed description', + content: 'deployed content', + language: 'bun', + kind: 'script', + draft: { + path: 'f/scripts/existing', + summary: 'db draft summary', + description: 'db draft description', + content: 'db draft content', + language: 'bun', + kind: 'script' + } + } as any) + + const raw = await callGlobalTool('read_workspace_item', { + type: 'script', + path: 'f/scripts/existing' + }) + const item = JSON.parse(raw) + + expect(item).toEqual({ + type: 'script', + path: 'f/scripts/existing', + summary: 'db draft summary', + language: 'bun', + value: 'db draft content', + isDraft: false + }) + expect(raw).not.toContain('deployed content') + }) + + it('reads saved DB flow drafts when no local draft exists', async () => { + vi.mocked(FlowService.getFlowByPathWithDraft).mockResolvedValueOnce({ + path: 'f/flows/existing', + summary: 'deployed summary', + value: { + modules: [{ id: 'deployed_step', value: { type: 'identity' } }] + }, + schema: { type: 'object', properties: { deployed: { type: 'boolean' } } }, + edited_by: 'admin', + edited_at: '2026-05-22T09:00:00Z', + archived: false, + extra_perms: {}, + draft: { + path: 'f/flows/existing', + summary: 'db draft summary', + value: { + modules: [{ id: 'draft_step', value: { type: 'identity' } }] + }, + schema: { type: 'object', properties: { draft: { type: 'string' } } }, + edited_by: 'admin', + edited_at: '2026-05-22T10:00:00Z', + archived: false, + extra_perms: {} + } + } as any) + + const raw = await callGlobalTool('read_workspace_item', { + type: 'flow', + path: 'f/flows/existing' + }) + const item = JSON.parse(raw) + + expect(item).toMatchObject({ + type: 'flow', + path: 'f/flows/existing', + summary: 'db draft summary', + isDraft: false, + value: { + modules: [{ id: 'draft_step', value: { type: 'identity' } }], + schema: { type: 'object', properties: { draft: { type: 'string' } } } + } + }) + expect(raw).not.toContain('deployed_step') + }) + it('applies path_prefix to local drafts before enforcing the result limit', async () => { await callGlobalTool('write_script', { path: 'f/other/outside', diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index 164ae4d4fc..59c00c9993 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -591,7 +591,9 @@ function itemMatches( ) } -function scriptToItem(script: Script, includeValue: boolean): WorkspaceItem { +type ReadableScript = Pick + +function scriptToItem(script: ReadableScript, includeValue: boolean): WorkspaceItem { return { type: 'script', path: script.path, @@ -602,7 +604,10 @@ function scriptToItem(script: Script, includeValue: boolean): WorkspaceItem { } } -function flowToItem(flow: Flow, includeValue: boolean): WorkspaceItem { +function flowToItem( + flow: Pick, + includeValue: boolean +): WorkspaceItem { return { type: 'flow', path: flow.path, @@ -614,6 +619,19 @@ function flowToItem(flow: Flow, includeValue: boolean): WorkspaceItem { } } +async function loadScriptWithDbDraft(path: string, workspace: string): Promise { + const script = await ScriptService.getScriptByPathWithDraft({ workspace, path }) + return script.draft ? { ...script, ...script.draft, path: script.path } : script +} + +async function loadFlowWithDbDraft( + path: string, + workspace: string +): Promise> { + const flow = await FlowService.getFlowByPathWithDraft({ workspace, path }) + return flow.draft ? { ...flow, ...flow.draft, path: flow.path } : flow +} + /** * Turn a flow workspace item into the compact response we send to the model: * rawscript content is replaced with `inline_script.` placeholders. @@ -1098,9 +1116,9 @@ async function readWorkspaceItem( ): Promise { switch (type) { case 'script': - return scriptToItem(await ScriptService.getScriptByPath({ workspace, path }), true) + return scriptToItem(await loadScriptWithDbDraft(path, workspace), true) case 'flow': - return flowToItem(await FlowService.getFlowByPath({ workspace, path }), true) + return flowToItem(await loadFlowWithDbDraft(path, workspace), true) case 'schedule': return scheduleToItem(await ScheduleService.getSchedule({ workspace, path }), true) case 'trigger': @@ -2327,7 +2345,7 @@ async function loadScriptForEdit( } return { content: draft.value, language: draft.language, summary: draft.summary } } - const script = await ScriptService.getScriptByPath({ workspace, path }) + const script = await loadScriptWithDbDraft(path, workspace) return { content: script.content, language: script.language, summary: script.summary } } @@ -2362,7 +2380,7 @@ async function loadFlowDraftValue( } return { flow: draft.value as FlowDraftValue, summary: draft.summary } } - const flow = await FlowService.getFlowByPath({ workspace, path }) + const flow = await loadFlowWithDbDraft(path, workspace) return { flow: { value: flow.value, schema: flow.schema, groups: flow.value.groups ?? null }, summary: flow.summary