diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index c2755f82a4..d7be9ec884 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -220,6 +220,7 @@ export let scriptLang: Preview['language'] | 'bunnative' export let disabled: boolean = false export let lineNumbersMinChars = 3 + export let isAiPanelOpen: boolean = false const rHash = randomHash() $: filePath = computePath(path) @@ -403,7 +404,7 @@ endColumn: edit.range.end.character + 1 }, text: edit.newText - } + } : {} ) //@ts-ignore @@ -464,16 +465,16 @@ scriptLang === 'postgresql' ? POSTGRES_TYPES : scriptLang === 'mysql' - ? MYSQL_TYPES - : scriptLang === 'snowflake' - ? SNOWFLAKE_TYPES - : scriptLang === 'bigquery' - ? BIGQUERY_TYPES - : scriptLang === 'mssql' - ? MSSQL_TYPES - : scriptLang === 'oracledb' - ? ORACLEDB_TYPES - : [] + ? MYSQL_TYPES + : scriptLang === 'snowflake' + ? SNOWFLAKE_TYPES + : scriptLang === 'bigquery' + ? BIGQUERY_TYPES + : scriptLang === 'mssql' + ? MSSQL_TYPES + : scriptLang === 'oracledb' + ? ORACLEDB_TYPES + : [] ).map((t) => ({ label: t, kind: languages.CompletionItemKind.Function, @@ -741,7 +742,7 @@ uri: vscode.Uri.parse(uri), name: 'windmill', index: 0 - } + } : undefined, initializationOptions, middleware: { @@ -1203,7 +1204,24 @@ }) editor?.addCommand(KeyMod.CtrlCmd | KeyCode.KeyL, function () { - dispatch('toggleAiPanel') + const selectedLines = getSelectedLines() + const selection = editor?.getSelection() + const hasSelection = + selection && + (selection.startLineNumber !== selection.endLineNumber || + selection.startColumn !== selection.endColumn) + if (hasSelection && selectedLines) { + dispatch('addSelectedLinesToAiChat', { + lines: selectedLines, + startLine: selection.startLineNumber, + endLine: selection.endLineNumber + }) + if (!isAiPanelOpen) { + dispatch('toggleAiPanel') + } + } else { + dispatch('toggleAiPanel') + } }) editor?.addCommand(KeyMod.CtrlCmd | KeyCode.KeyU, function () { diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 440e6eb824..b21819e2ed 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -706,7 +706,7 @@ }) } } - ] + ] : []), ...(!script.draft_only ? [ @@ -716,9 +716,9 @@ dispatch('seeDetails', initialPath) } } - ] + ] : []) - ] + ] : [] return dropdownItems.length > 0 ? dropdownItems : undefined diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index ecdb6b57df..296276a0c2 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -302,6 +302,7 @@ let storedAiPanelSize = aiPanelSize > 0 ? aiPanelSize : 30 let testPanelSize = 30 let storedTestPanelSize = testPanelSize + function toggleAiPanel() { if (!$copilotInfo.enabled) return if (aiPanelSize > 0) { @@ -316,6 +317,15 @@ } } + function addSelectedLinesToAiChat( + e: CustomEvent<{ lines: string; startLine: number; endLine: number }> + ) { + if (aiChat) { + aiChat.addSelectedLinesToContext(e.detail.lines, e.detail.startLine, e.detail.endLine) + aiChat.focusTextArea() + } + } + $: !SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(lang ?? '') && aiPanelSize > 0 && toggleAiPanel() function toggleTestPanel() { @@ -506,7 +516,9 @@ }} on:saveDraft on:toggleAiPanel={toggleAiPanel} + on:addSelectedLinesToAiChat={addSelectedLinesToAiChat} on:toggleTestPanel={toggleTestPanel} + isAiPanelOpen={aiPanelSize > 0} cmdEnterAction={async () => { await inferSchema(code) runTest() diff --git a/frontend/src/lib/components/copilot/chat/AIChat.svelte b/frontend/src/lib/components/copilot/chat/AIChat.svelte index 8b5af67f45..fa83e72346 100644 --- a/frontend/src/lib/components/copilot/chat/AIChat.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChat.svelte @@ -193,7 +193,11 @@ } selectedContext = selectedContext - .map((c) => availableContext.find((ac) => ac.type === c.type && ac.title === c.title)) + .map((c) => + c.type === 'code_piece' && code.includes(c.content) + ? c + : availableContext.find((ac) => ac.type === c.type && ac.title === c.title) + ) .filter((c) => c !== undefined) as ContextElement[] } catch (err) { console.error('Could not update available context', err) @@ -209,7 +213,7 @@ type: 'db', title: c.title, schema: dbSchemas[c.title] - } + } : c ) as ContextElement[] })) @@ -277,7 +281,9 @@ return } try { + // Remove code pieces from the context to not include them on the next request const oldSelectedContext = selectedContext + selectedContext = selectedContext.filter((c) => c.type !== 'code_piece') if (options.removeDiff) { selectedContext = selectedContext.filter((c) => c.type !== 'diff') } @@ -323,7 +329,7 @@ { role: 'assistant', content: $currentReply, - contextElements: selectedContext.filter((c) => c.type === 'code') + contextElements: oldSelectedContext.filter((c) => c.type === 'code') } ] currentReply.set('') @@ -387,6 +393,27 @@ } } + export function addSelectedLinesToContext(lines: string, startLine: number, endLine: number) { + if ( + selectedContext.find( + (c) => c.type === 'code_piece' && c.title === `L${startLine}-L${endLine}` + ) + ) { + return + } + selectedContext = [ + ...selectedContext, + { + type: 'code_piece', + title: `L${startLine}-L${endLine}`, + startLine, + endLine, + content: lines, + lang + } + ] + } + export function fix() { if (!contextCodePath) { return @@ -430,7 +457,7 @@ diff: diffLines(lastDeployedCode ?? '', code), lang } - ] + ] : []) ] sendRequest({ @@ -442,6 +469,10 @@ } } + export function focusTextArea() { + aiChatDisplay?.focusInput() + } + interface ChatSchema extends IDBSchema { chats: { key: string @@ -470,10 +501,13 @@ const chats = await indexDB.getAll('chats') console.log('Retrieved chats') - savedChats = chats.reduce((acc, chat) => { - acc[chat.id] = chat - return acc - }, {} as typeof savedChats) + savedChats = chats.reduce( + (acc, chat) => { + acc[chat.id] = chat + return acc + }, + {} as typeof savedChats + ) } catch (err) { console.error('Could not open chat history database', err) } @@ -502,7 +536,7 @@ content: $currentReply, contextElements: selectedContext.filter((c) => c.type === 'code') } - ] + ] : displayMessages} bind:instructions on:sendRequest={() => sendRequest()} diff --git a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte index e0cc7ac32a..f0d9a6c287 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte @@ -35,6 +35,12 @@ automaticScroll = true } + let contextTextareaComponent: ContextTextarea + + export function focusInput() { + contextTextareaComponent?.focus() + } + let automaticScroll = true let scrollEl: HTMLDivElement async function scrollDown() { @@ -199,23 +205,19 @@ {#each selectedContext as element} - {@const contextElement = availableContext.find( - (c) => c.type === element.type && c.title === element.title - )} - {#if contextElement} - { - selectedContext = selectedContext.filter( - (c) => c.type !== element.type || c.title !== element.title - ) - }} - /> - {/if} + { + selectedContext = selectedContext.filter( + (c) => c.type !== element.type || c.title !== element.title + ) + }} + /> {/each} Not loaded yet {/if} - {:else if contextElement.type === 'code' || contextElement.type === 'diff'} + {:else if contextElement.type === 'code' || contextElement.type === 'code_piece' || contextElement.type === 'diff'}
diff --git a/frontend/src/lib/components/copilot/chat/core.ts b/frontend/src/lib/components/copilot/chat/core.ts index 2d5d2f669f..d315b7c65d 100644 --- a/frontend/src/lib/components/copilot/chat/core.ts +++ b/frontend/src/lib/components/copilot/chat/core.ts @@ -211,6 +211,7 @@ export const CHAT_SYSTEM_PROMPT = ` When the user requests code changes: - Always include a **single code block** with the **entire updated file**, not just the modified sections. + - The code can include \`[#START]\` and \`[#END]\` markers to indicate the start and end of a code piece. You MUST only modify the code between these markers if given, and remove them in your response. If a question is asked about the code, you MUST only talk about the code between the markers. Refer to it as the code piece, not the code between the markers. - Follow the instructions carefully and explain the reasoning behind your changes. - If the request is abstract (e.g., "make this cleaner"), interpret it concretely and reflect that in the code block. - Preserve existing formatting, indentation, and whitespace unless changes are strictly required to fulfill the user's request. @@ -223,7 +224,7 @@ export const CHAT_SYSTEM_PROMPT = ` ` const CHAT_USER_CODE_CONTEXT = ` -CODE ({title}): +- {title}: \`\`\`{language} {code} \`\`\` @@ -241,19 +242,6 @@ INSTRUCTIONS: WINDMILL LANGUAGE CONTEXT: {lang_context} -DATABASES: -{db_context} - -CODE: -{code_context} - -ERROR: -{error_context} - -DIFF: -{diff_context} - -\`\`\` ` export const CHAT_USER_DB_CONTEXT = `- {title}: SCHEMA: \n{schema}\n` @@ -278,71 +266,120 @@ export const ContextIconMap = { code: Code, error: TriangleAlert, db: Database, - diff: Diff + diff: Diff, + code_piece: Code } -export type ContextElement = - | { - type: 'code' - content: string - title: string - lang: ScriptLang | 'bunnative' - } - | { - type: 'error' - content: string - title: 'error' - } - | { - type: 'db' - schema?: DBSchema - title: string - } - | { - type: 'diff' - content: string - title: string - diff: Change[] - lang: ScriptLang | 'bunnative' - } +type CodeElement = { + type: 'code' + content: string + title: string + lang: ScriptLang | 'bunnative' +} + +type ErrorElement = { + type: 'error' + content: string + title: 'error' +} + +type DBElement = { + type: 'db' + schema?: DBSchema + title: string +} + +type DiffElement = { + type: 'diff' + content: string + title: string + diff: Change[] + lang: ScriptLang | 'bunnative' +} + +type CodePieceElement = { + type: 'code_piece' + content: string + startLine: number + endLine: number + title: string + lang: ScriptLang | 'bunnative' +} + +export type ContextElement = CodeElement | ErrorElement | DBElement | DiffElement | CodePieceElement + +const applyCodePieceToCodeContext = (codePieces: CodePieceElement[], codeContext: string) => { + let code = codeContext.split('\n') + let shiftOffset = 0 + codePieces.sort((a, b) => a.startLine - b.startLine) + for (const codePiece of codePieces) { + code.splice(codePiece.endLine + shiftOffset, 0, '[#END]') + code.splice(codePiece.startLine + shiftOffset - 1, 0, '[#START]') + shiftOffset += 2 + } + return code.join('\n') +} export async function prepareUserMessage( instructions: string, language: ScriptLang | 'bunnative', selectedContext: ContextElement[] ) { - let codeContext = '' - let errorContext = '' - let dbContext = '' - let diffContext = '' + let codeContext = 'CODE:\n' + let errorContext = 'ERROR:\n' + let dbContext = 'DATABASES:\n' + let diffContext = 'DIFF:\n' + let hasCode = false + let hasError = false + let hasDb = false + let hasDiff = false for (const context of selectedContext) { if (context.type === 'code') { + hasCode = true codeContext += CHAT_USER_CODE_CONTEXT.replace('{title}', context.title) .replace('{language}', scriptLangToEditorLang(language)) - .replace('{code}', context.content) + .replace( + '{code}', + applyCodePieceToCodeContext( + selectedContext.filter((c) => c.type === 'code_piece'), + context.content + ) + ) } else if (context.type === 'error') { - if (errorContext) { + if (hasError) { throw new Error('Multiple error contexts provided') } + hasError = true errorContext = CHAT_USER_ERROR_CONTEXT.replace('{error}', context.content) } else if (context.type === 'db') { + hasDb = true dbContext += CHAT_USER_DB_CONTEXT.replace('{title}', context.title).replace( '{schema}', context.schema?.stringified ?? 'to fetch with get_db_schema' ) } else if (context.type === 'diff') { + hasDiff = true const diff = JSON.stringify(context.diff) diffContext = diff.length > 3000 ? diff.slice(0, 3000) + '...' : diff } } - const userMessage = CHAT_USER_PROMPT.replace('{instructions}', instructions) - .replace('{lang_context}', getLangContext(language)) - .replace('{code_context}', codeContext) - .replace('{error_context}', errorContext) - .replace('{db_context}', dbContext) - .replace('{diff_context}', diffContext) - + let userMessage = CHAT_USER_PROMPT.replace('{instructions}', instructions).replace( + '{lang_context}', + getLangContext(language) + ) + if (hasCode) { + userMessage += codeContext + } + if (hasError) { + userMessage += errorContext + } + if (hasDb) { + userMessage += dbContext + } + if (hasDiff) { + userMessage += diffContext + } return userMessage } diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index ec0e1268c8..5168b184c4 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -154,7 +154,7 @@ export const copilotSessionModel = writable( ? { model: sessionModel, provider: sessionProvider as AIProvider - } + } : undefined ) export const usedTriggerKinds = writable([])