feat: handle sending selected lines to ai context (#5527)

* allow mentioning specific files in instructions

* remove not working highlight implementation

* make highlighted text work

* fix tooltip position

* clean code

* cleaning

* use lib for tooltip positioning

* fix logic

* draft for db in context

* use tools for db in context

* fixes

* cleaning and bug fixes

* fix

* cleaning

* fix when script is db type

* simplify logic

* put schema in context if already here

* fix imports

* fix tooltip position and make it scrollable

* remove console logs

* check if selected is in available

* fix tooltip list

* add back lost logic

* last fix

* fix type errors

* use loaded schema from dbSchemas

* fix typing, content and lang are always there

* remove from context if not available anymore

* add not loaded yet mention if schema not loaded

* add missing callback logic

* fix prompt

* fix usage of updateselectedContext function

* fix styling for white theme

* handle tab and arrows

* fix schemas not being refreshed on contexts

* also refresh displayMessages when dbschemas change

* fix duplicate available contexts

* fix logic for new scripts

* fix new lines inside text area

* implement sending diff in context

* add button in deploy options to ask ai about diff

* also visualize change when asking for diff

* better prompt

* add limit to diff size

* put diff mode toggle in editor bar

* add button to see history from editor

* adjustements

* put see diff button in dropdown

* fixes

* better styling

* handle adding code piece to context

* add code piece in context

* draft start end markers

* adapt code

* draft

* apply code pieces before sending request

* cleaning

* highlight if diff mode

* format files

* change buttons based on diffmode

* remove diff after sending message

* fix type error

* smaller buttons

* draft

* use existing editor in diff editor

* fix number of db resources fetches

* fix apply and add buttons on diff mode

* cleaning

* undo ai gen button show

* better buttons

* better prompt

* remove console log

* fix merge

* avoid duplicates

* fix merge

* fix

* fix apply logic

* remove useless if

* focus text area + close chat if no selected lines

---------

Co-authored-by: HugoCasa <hugo@casademont.ch>
This commit is contained in:
centdix
2025-04-04 11:13:24 +02:00
committed by GitHub
parent 6602c5fedf
commit 097373ad5d
9 changed files with 200 additions and 93 deletions

View File

@@ -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 () {

View File

@@ -706,7 +706,7 @@
})
}
}
]
]
: []),
...(!script.draft_only
? [
@@ -716,9 +716,9 @@
dispatch('seeDetails', initialPath)
}
}
]
]
: [])
]
]
: []
return dropdownItems.length > 0 ? dropdownItems : undefined

View File

@@ -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()

View File

@@ -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()}

View File

@@ -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 @@
</svelte:fragment>
</Popover>
{#each selectedContext as element}
{@const contextElement = availableContext.find(
(c) => c.type === element.type && c.title === element.title
)}
{#if contextElement}
<ContextElementBadge
{contextElement}
deletable
on:delete={() => {
selectedContext = selectedContext.filter(
(c) => c.type !== element.type || c.title !== element.title
)
}}
/>
{/if}
<ContextElementBadge
contextElement={element}
deletable
on:delete={() => {
selectedContext = selectedContext.filter(
(c) => c.type !== element.type || c.title !== element.title
)
}}
/>
{/each}
</div>
<ContextTextarea
bind:this={contextTextareaComponent}
{instructions}
{availableContext}
{selectedContext}

View File

@@ -68,7 +68,7 @@
<div class="text-tertiary">Not loaded yet</div>
{/if}
</div>
{:else if contextElement.type === 'code' || contextElement.type === 'diff'}
{:else if contextElement.type === 'code' || contextElement.type === 'code_piece' || contextElement.type === 'diff'}
<div class="max-w-96 max-h-[300px] text-xs overflow-auto">
<HighlightCode
language={contextElement.lang}

View File

@@ -286,6 +286,10 @@
}
$: updateTooltipPosition(availableContext, showContextTooltip, contextTooltipWord)
export function focus() {
textarea?.focus()
}
</script>
<div class="relative w-full px-2 scroll-pb-2">

View File

@@ -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
}

View File

@@ -154,7 +154,7 @@ export const copilotSessionModel = writable<AIProviderModel | undefined>(
? {
model: sessionModel,
provider: sessionProvider as AIProvider
}
}
: undefined
)
export const usedTriggerKinds = writable<string[]>([])