From 7196568e8692306462049e1a12e4792d0bc1acef Mon Sep 17 00:00:00 2001 From: centdix Date: Wed, 20 May 2026 15:10:22 +0200 Subject: [PATCH] fix: read draft-only scripts in global chat --- .../copilot/chat/global/core.test.ts | 39 +++++++++++++++++-- .../components/copilot/chat/global/core.ts | 31 +++++++++++++-- 2 files changed, 64 insertions(+), 6 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 f62572db02..182af476f8 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.test.ts @@ -53,6 +53,7 @@ vi.mock('$lib/gen', async () => { ScriptService: wrapService(actual.ScriptService, { existsScriptByPath: vi.fn(async () => false), getScriptByPath: vi.fn(), + getScriptByPathWithDraft: vi.fn(), listScripts: vi.fn(async () => []) }), FlowService: wrapService(actual.FlowService, { @@ -227,10 +228,42 @@ describe('global AI tools', () => { summary: 'Hello script', language: 'bun', value: content, - isDraft: false + isDraft: true }) }) + it('reads backend draft-only scripts through the draft-aware endpoint', async () => { + const content = 'export async function main() {\n\treturn "db draft"\n}' + vi.mocked(ScriptService.getScriptByPathWithDraft).mockResolvedValueOnce({ + hash: 'draft-hash', + path: 'f/scripts/backend-draft', + summary: 'Backend draft', + description: '', + content, + language: 'bun', + draft_only: true + } as any) + + const raw = await callGlobalTool('read_workspace_item', { + type: 'script', + path: 'f/scripts/backend-draft' + }) + + expect(JSON.parse(raw)).toMatchObject({ + type: 'script', + path: 'f/scripts/backend-draft', + summary: 'Backend draft', + language: 'bun', + value: content, + isDraft: true + }) + expect(ScriptService.getScriptByPathWithDraft).toHaveBeenCalledWith({ + workspace: WORKSPACE, + path: 'f/scripts/backend-draft' + }) + expect(ScriptService.getScriptByPath).not.toHaveBeenCalled() + }) + 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('script', 'f/scripts/open-editor', { workspace: WORKSPACE }) @@ -631,7 +664,7 @@ describe('global AI tools', () => { schedule: '0 0 12 * * *', timezone: 'UTC' }), - isDraft: false + isDraft: true }) }) @@ -794,7 +827,7 @@ describe('global AI tools', () => { }) const item = JSON.parse(raw) - expect(item.isDraft).toBe(false) + expect(item.isDraft).toBe(true) expect(item.value).toMatchObject({ modules: [ { diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index 988e1a716f..8d6d1ac83a 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -25,6 +25,7 @@ import type { ListableVariable, NewSchedule, NewScript, + NewScriptWithDraft, Schedule, Script, ScriptLang @@ -523,17 +524,30 @@ function itemMatchesPathPrefix( return !pathPrefix || item.path.startsWith(pathPrefix) } -function scriptToItem(script: Script, includeValue: boolean): WorkspaceItem { +type ScriptItemSource = Pick & { + draft_only?: boolean +} + +function scriptToItem( + script: ScriptItemSource, + includeValue: boolean, + isDraft = script.draft_only === true +): WorkspaceItem { return { type: 'script', path: script.path, summary: script.summary, language: script.language, value: includeValue ? script.content : undefined, - isDraft: false + isDraft } } +function scriptWithDraftToItem(script: NewScriptWithDraft, includeValue: boolean): WorkspaceItem { + const draft = script.draft + return scriptToItem(draft ?? script, includeValue, draft !== undefined || script.draft_only === true) +} + function flowToItem(flow: Flow, includeValue: boolean): WorkspaceItem { return { type: 'flow', @@ -964,7 +978,10 @@ async function readWorkspaceItem( ): Promise { switch (type) { case 'script': - return scriptToItem(await ScriptService.getScriptByPath({ workspace, path }), true) + return scriptWithDraftToItem( + await ScriptService.getScriptByPathWithDraft({ workspace, path }), + true + ) case 'flow': return flowToItem(await FlowService.getFlowByPath({ workspace, path }), true) case 'schedule': @@ -1322,6 +1339,14 @@ export const globalTools: Tool<{}>[] = [ toolCallbacks.setToolStatus(toolId, { content: message, error: message }) return JSON.stringify({ success: false, error: message }) } + const draftItem = getGlobalDraft(workspace, parsed.type, parsed.path, parsed.trigger_kind) + if (draftItem) { + toolCallbacks.setToolStatus(toolId, { + content: `Read AI draft ${parsed.type} "${parsed.path}"` + }) + return JSON.stringify(serializeWorkspaceItemForRead(draftItem), null, 2) + } + const currentItem = getGlobalCurrentItem( workspace, parsed.type,