mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 08:05:44 +00:00
feat: use FIM for code autocomplete (#6081)
* feat: use FIM for code autocomplete * nits
This commit is contained in:
Generated
+20
-9
@@ -46,6 +46,7 @@
|
||||
"hash-sum": "^2.0.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"idb": "^8.0.2",
|
||||
"lru-cache": "^11.1.0",
|
||||
"lucide-svelte": "^0.399.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~16.1.1",
|
||||
@@ -7050,6 +7051,20 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/html-tags": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
|
||||
@@ -7822,16 +7837,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/ltgt": {
|
||||
|
||||
@@ -91,9 +91,10 @@
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@scalar/openapi-parser": "^0.15.0",
|
||||
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
|
||||
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
|
||||
"@windmill-labs/svelte-dnd-action": "^0.9.48",
|
||||
"@xyflow/svelte": "^1.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xyflow/svelte": "^1.0.0",
|
||||
"ag-charts-community": "^9.0.1",
|
||||
"ag-charts-enterprise": "^9.0.1",
|
||||
"ag-grid-community": "^31.3.4",
|
||||
@@ -112,6 +113,7 @@
|
||||
"hash-sum": "^2.0.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"idb": "^8.0.2",
|
||||
"lru-cache": "^11.1.0",
|
||||
"lucide-svelte": "^0.399.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~16.1.1",
|
||||
@@ -133,7 +135,6 @@
|
||||
"svelte-exmarkdown": "^5.0.0",
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"vscode": "npm:@codingame/monaco-vscode-extension-api@~16.1.1",
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
extToLang
|
||||
} from '$lib/editorUtils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { type Preview, ResourceService, UserService } from '$lib/gen'
|
||||
import { type Preview, ResourceService, type ScriptLang, UserService } from '$lib/gen'
|
||||
import type { Text } from 'yjs'
|
||||
import { initializeVscode, keepModelAroundToAvoidDisposalOfWorkers } from '$lib/components/vscode'
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
import * as htmllang from '$lib/svelteMonarch'
|
||||
import { conf, language } from '$lib/vueMonarch'
|
||||
|
||||
import { Autocompletor } from './copilot/autocomplete/monaco-adapter'
|
||||
import { Autocompletor } from './copilot/autocomplete/Autocompletor'
|
||||
import { AIChatEditorHandler } from './copilot/chat/monaco-adapter'
|
||||
import GlobalReviewButtons from './copilot/chat/GlobalReviewButtons.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
@@ -650,79 +650,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: $reviewingChanges && autocompletor?.reject()
|
||||
|
||||
let completorDisposable: IDisposable | undefined = undefined
|
||||
let autocompletor: Autocompletor | undefined = undefined
|
||||
function addSuperCompletor(editor: meditor.IStandaloneCodeEditor) {
|
||||
try {
|
||||
if (completorDisposable) {
|
||||
completorDisposable.dispose()
|
||||
}
|
||||
if (!scriptLang) {
|
||||
throw new Error('No script lang')
|
||||
}
|
||||
autocompletor = new Autocompletor(editor, lang, scriptLang)
|
||||
|
||||
// last user events (currently disabled):
|
||||
// let lastTs = Date.now()
|
||||
// editor.onDidChangeModelContent((e) => {
|
||||
// const thisTs = Date.now()
|
||||
// lastTs = thisTs
|
||||
// setTimeout(() => {
|
||||
// if (thisTs === lastTs) {
|
||||
// autocompletor?.savePatch()
|
||||
// }
|
||||
// }, 150)
|
||||
// })
|
||||
|
||||
completorDisposable = editor.onDidChangeCursorPosition((e) => {
|
||||
autocompletor?.reject()
|
||||
if ($reviewingChanges) {
|
||||
return
|
||||
}
|
||||
const position = editor.getPosition()
|
||||
if (!position) {
|
||||
return
|
||||
}
|
||||
const upToText = editor.getModel()?.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: 0,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column
|
||||
})
|
||||
const lastChar = upToText ? upToText[upToText.length - 1] : ''
|
||||
if (lastChar && lastChar.match(/[\(\{\s:="',]/)) {
|
||||
autocompletor?.predict()
|
||||
}
|
||||
})
|
||||
|
||||
editor.onKeyDown((e) => {
|
||||
if (e.keyCode === KeyCode.Escape) {
|
||||
autocompletor?.reject()
|
||||
} else if (e.keyCode === KeyCode.Tab && autocompletor?.hasChanges()) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
autocompletor?.accept()
|
||||
autocompletor?.predict()
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Could not add supercompletor', err)
|
||||
function addAutoCompletor(
|
||||
editor: meditor.IStandaloneCodeEditor,
|
||||
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
|
||||
) {
|
||||
if (autocompletor) {
|
||||
autocompletor.dispose()
|
||||
}
|
||||
autocompletor = new Autocompletor(editor, scriptLang)
|
||||
}
|
||||
|
||||
$: $copilotInfo.enabled &&
|
||||
$copilotInfo.codeCompletionModel &&
|
||||
$codeCompletionSessionEnabled &&
|
||||
Autocompletor.isProviderModelSupported($copilotInfo.codeCompletionModel) &&
|
||||
initialized &&
|
||||
editor &&
|
||||
scriptLang &&
|
||||
addSuperCompletor(editor)
|
||||
addAutoCompletor(editor, scriptLang)
|
||||
|
||||
$: $copilotInfo.enabled && initialized && editor && addChatHandler(editor)
|
||||
|
||||
$: !$codeCompletionSessionEnabled && (completorDisposable?.dispose(), autocompletor?.reject())
|
||||
$: !$codeCompletionSessionEnabled && autocompletor?.dispose()
|
||||
|
||||
const outputChannel = {
|
||||
name: 'Language Server Client',
|
||||
@@ -1525,7 +1475,7 @@
|
||||
disposeMethod && disposeMethod()
|
||||
websocketInterval && clearInterval(websocketInterval)
|
||||
sqlSchemaCompletor && sqlSchemaCompletor.dispose()
|
||||
completorDisposable && completorDisposable.dispose()
|
||||
autocompletor && autocompletor.dispose()
|
||||
sqlTypeCompletor && sqlTypeCompletor.dispose()
|
||||
timeoutModel && clearTimeout(timeoutModel)
|
||||
loadTimeout && clearTimeout(loadTimeout)
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import type { AIProviderModel, ScriptLang } from '$lib/gen'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { Position, type editor as meditor, languages, type IDisposable } from 'monaco-editor'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import { autocompleteRequest } from './request'
|
||||
import { FIM_MAX_TOKENS } from '../lib'
|
||||
|
||||
type CacheCompletion = {
|
||||
linePrefix: string
|
||||
completion: string
|
||||
column: number
|
||||
}
|
||||
|
||||
function filterCompletion(completion: string, suffix: string): string | undefined {
|
||||
const trimmedCompletion = completion.replaceAll('\n', '')
|
||||
const trimmedSuffix = suffix.slice(0, FIM_MAX_TOKENS).replaceAll('\n', '')
|
||||
|
||||
if (trimmedSuffix.startsWith(trimmedCompletion)) {
|
||||
console.log('suffix starts with completion', suffix, completion)
|
||||
return
|
||||
}
|
||||
return completion
|
||||
}
|
||||
|
||||
export class Autocompletor {
|
||||
#lastTs = Date.now()
|
||||
#cache: LRUCache<number, CacheCompletion> = new LRUCache({
|
||||
max: 10
|
||||
})
|
||||
#scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
|
||||
#abortController: AbortController = new AbortController()
|
||||
#completionDisposable: IDisposable
|
||||
#cursorDisposable: IDisposable
|
||||
|
||||
constructor(
|
||||
editor: meditor.IStandaloneCodeEditor,
|
||||
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
|
||||
) {
|
||||
this.#scriptLang = scriptLang
|
||||
|
||||
this.#completionDisposable = languages.registerInlineCompletionsProvider(
|
||||
{ pattern: '**' },
|
||||
{
|
||||
provideInlineCompletions: async (model, position, context, token) => {
|
||||
if (
|
||||
token.isCancellationRequested ||
|
||||
model.uri.toString() !== editor.getModel()?.uri.toString()
|
||||
) {
|
||||
return { items: [] }
|
||||
}
|
||||
const result = await this.#autocomplete(model, position)
|
||||
|
||||
if (result) {
|
||||
const completion = filterCompletion(result.completion, result.suffix)
|
||||
|
||||
if (!completion) {
|
||||
return { items: [] }
|
||||
}
|
||||
|
||||
let range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: position.column,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column
|
||||
}
|
||||
|
||||
const multiline = completion.indexOf('\n') !== -1
|
||||
if (multiline) {
|
||||
// if multiline the range should span until the end of the line
|
||||
range.endColumn = model.getLineMaxColumn(position.lineNumber)
|
||||
}
|
||||
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
insertText: completion,
|
||||
range
|
||||
}
|
||||
]
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
items: []
|
||||
}
|
||||
}
|
||||
},
|
||||
freeInlineCompletions: () => {}
|
||||
}
|
||||
)
|
||||
|
||||
this.#cursorDisposable = editor.onDidChangeCursorPosition(async (e) => {
|
||||
if (e.source === 'mouse') {
|
||||
const model = editor.getModel()
|
||||
if (model) {
|
||||
this.#autocomplete(model, e.position)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static isProviderModelSupported(providerModel: AIProviderModel | undefined) {
|
||||
return (
|
||||
providerModel &&
|
||||
providerModel.provider === 'mistral' &&
|
||||
providerModel.model.startsWith('codestral-') &&
|
||||
!providerModel.model.startsWith('codestral-embed')
|
||||
)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.#completionDisposable.dispose()
|
||||
this.#cursorDisposable.dispose()
|
||||
}
|
||||
|
||||
async #autocomplete(
|
||||
model: meditor.ITextModel,
|
||||
position: Position
|
||||
): Promise<{ completion: string; suffix: string } | undefined> {
|
||||
const thisTs = Date.now()
|
||||
this.#lastTs = thisTs
|
||||
|
||||
await sleep(200)
|
||||
|
||||
if (model.isDisposed()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (thisTs !== this.#lastTs) {
|
||||
return
|
||||
}
|
||||
|
||||
const linePrefix = model.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column
|
||||
})
|
||||
|
||||
const suffix = model.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: position.column,
|
||||
endLineNumber: model.getLineCount(),
|
||||
endColumn: model.getLineMaxColumn(model.getLineCount())
|
||||
})
|
||||
|
||||
const cachedCompletion = this.#cache.get(position.lineNumber)
|
||||
if (cachedCompletion) {
|
||||
if (
|
||||
position.column > cachedCompletion.column &&
|
||||
linePrefix.length < cachedCompletion.linePrefix.length + cachedCompletion.completion.length
|
||||
) {
|
||||
const completeLine = cachedCompletion.linePrefix + cachedCompletion.completion
|
||||
const newLinePrefix = completeLine.substring(0, position.column - 1)
|
||||
if (newLinePrefix === linePrefix) {
|
||||
const modifiedCompletion = cachedCompletion.completion.slice(
|
||||
position.column - cachedCompletion.column
|
||||
)
|
||||
console.debug('autocomplete partial cache hit', modifiedCompletion)
|
||||
return { completion: modifiedCompletion, suffix }
|
||||
}
|
||||
} else if (
|
||||
position.column === cachedCompletion.column &&
|
||||
cachedCompletion.linePrefix === linePrefix
|
||||
) {
|
||||
console.debug('autocomplete exact cache hit', cachedCompletion.completion)
|
||||
return { completion: cachedCompletion.completion, suffix }
|
||||
}
|
||||
}
|
||||
|
||||
this.#abortController.abort()
|
||||
this.#abortController = new AbortController()
|
||||
const prefix = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column
|
||||
})
|
||||
|
||||
const completion = await autocompleteRequest(
|
||||
{
|
||||
prefix,
|
||||
suffix,
|
||||
scriptLang: this.#scriptLang
|
||||
},
|
||||
this.#abortController
|
||||
)
|
||||
|
||||
if (!completion) {
|
||||
return
|
||||
}
|
||||
|
||||
this.#cache.set(position.lineNumber, {
|
||||
linePrefix,
|
||||
completion,
|
||||
column: position.column
|
||||
})
|
||||
|
||||
return { completion, suffix }
|
||||
}
|
||||
}
|
||||
@@ -1,576 +0,0 @@
|
||||
import { type Change, createTwoFilesPatch, diffLines, diffWordsWithSpace } from 'diff'
|
||||
import { type editor as meditor } from 'monaco-editor'
|
||||
import { autocompleteRequest } from './request'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { displayVisualChanges, getLines, setGlobalCSS, type VisualChange } from '../shared'
|
||||
import type { ScriptLang } from '$lib/gen'
|
||||
|
||||
function lineChangesToVisualChanges(changes: Change[], startLineNumber: number) {
|
||||
let originalLineNumber = startLineNumber
|
||||
let visualChanges: VisualChange[] = []
|
||||
|
||||
let removedLines: string[] = []
|
||||
|
||||
for (const c of changes) {
|
||||
if (c.removed) {
|
||||
const lines = getLines(c.value)
|
||||
originalLineNumber += lines.length
|
||||
removedLines.push(...lines)
|
||||
} else if (c.added) {
|
||||
const newLines = getLines(c.value)
|
||||
const removedStartLineNumber = originalLineNumber - removedLines.length
|
||||
let afterLines: string[] = []
|
||||
for (const [idx, newLine] of newLines.entries()) {
|
||||
const originalLine = removedLines[idx]
|
||||
if (originalLine !== undefined) {
|
||||
const lineDiff = diffWordsWithSpace(originalLine, newLine)
|
||||
const firstRemovedChangeIdx = lineDiff.findIndex((c) => c.removed)
|
||||
if (firstRemovedChangeIdx !== -1 && lineDiff.length > 3) {
|
||||
let startColumn = 1
|
||||
let newLineContent = newLine
|
||||
const firstChange = lineDiff[0]
|
||||
if (
|
||||
!firstChange.added &&
|
||||
!firstChange.removed &&
|
||||
firstChange.value.trim().length === 0
|
||||
) {
|
||||
startColumn += firstChange.value.length
|
||||
newLineContent = newLineContent.slice(firstChange.value.length)
|
||||
}
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: removedStartLineNumber + idx,
|
||||
startColumn,
|
||||
endLine: removedStartLineNumber + idx,
|
||||
endColumn: 10000
|
||||
}
|
||||
})
|
||||
visualChanges.push({
|
||||
type: 'added_inline',
|
||||
position: {
|
||||
line: removedStartLineNumber + idx,
|
||||
column: 10000
|
||||
},
|
||||
value: newLineContent,
|
||||
options: {
|
||||
greenHighlight: true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let col = 1
|
||||
let removedChars = 0
|
||||
for (const charChange of lineDiff) {
|
||||
if (charChange.added) {
|
||||
visualChanges.push({
|
||||
type: 'added_inline',
|
||||
position: {
|
||||
line: removedStartLineNumber + idx,
|
||||
column: col
|
||||
},
|
||||
value: charChange.value,
|
||||
options: {
|
||||
greenHighlight: removedChars > 0
|
||||
}
|
||||
})
|
||||
removedChars = Math.max(0, removedChars - charChange.value.length)
|
||||
} else if (charChange.removed) {
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: removedStartLineNumber + idx,
|
||||
startColumn: col,
|
||||
endLine: removedStartLineNumber + idx,
|
||||
endColumn: col + charChange.value.length
|
||||
}
|
||||
})
|
||||
removedChars += charChange.value.length
|
||||
col += charChange.value.length
|
||||
} else {
|
||||
col += charChange.value.length
|
||||
removedChars = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
afterLines.push(newLine)
|
||||
}
|
||||
}
|
||||
if (afterLines.length > 0) {
|
||||
visualChanges.push({
|
||||
type: 'added_block',
|
||||
position: {
|
||||
afterLineNumber: originalLineNumber - 1
|
||||
},
|
||||
value: afterLines.join('\n')
|
||||
})
|
||||
}
|
||||
if (removedLines.length > newLines.length) {
|
||||
for (let i = 0; i < removedLines.length - newLines.length; i++) {
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: removedStartLineNumber + newLines.length + i,
|
||||
startColumn: 0,
|
||||
endLine: removedStartLineNumber + newLines.length + i,
|
||||
endColumn: 100000
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
removedLines = []
|
||||
} else {
|
||||
if (removedLines.length > 0) {
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: originalLineNumber - removedLines.length,
|
||||
startColumn: 0,
|
||||
endLine: originalLineNumber - 1,
|
||||
endColumn: 10000
|
||||
}
|
||||
})
|
||||
}
|
||||
originalLineNumber += c.count!
|
||||
removedLines = []
|
||||
}
|
||||
}
|
||||
if (removedLines.length > 0) {
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: originalLineNumber - removedLines.length,
|
||||
startColumn: 0,
|
||||
endLine: originalLineNumber - 1,
|
||||
endColumn: 10000
|
||||
}
|
||||
})
|
||||
}
|
||||
return visualChanges
|
||||
}
|
||||
|
||||
const MAX_PATCHES = 4
|
||||
|
||||
export class Autocompletor {
|
||||
editor: meditor.IStandaloneCodeEditor
|
||||
language: string
|
||||
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
|
||||
viewZoneIds: string[] = []
|
||||
decorationsCollection: meditor.IEditorDecorationsCollection | undefined = undefined
|
||||
visualChanges: VisualChange[] = []
|
||||
modifiedCode: string = ''
|
||||
applyZone:
|
||||
| {
|
||||
startLineNumber: number
|
||||
endLineNumber: number
|
||||
}
|
||||
| undefined = undefined
|
||||
lastChangePosition:
|
||||
| {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
| undefined = undefined
|
||||
|
||||
abortController: AbortController | undefined = undefined
|
||||
lastTs = Date.now()
|
||||
|
||||
lastCodeValue: string
|
||||
patches: string[] = []
|
||||
|
||||
predictedChange:
|
||||
| {
|
||||
position: {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
distance: number
|
||||
}
|
||||
| undefined = undefined
|
||||
tabWidget: meditor.IContentWidget | undefined = undefined
|
||||
|
||||
constructor(
|
||||
editor: meditor.IStandaloneCodeEditor,
|
||||
language: string,
|
||||
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
|
||||
) {
|
||||
this.editor = editor
|
||||
this.language = language
|
||||
this.scriptLang = scriptLang
|
||||
this.lastCodeValue = editor.getModel()?.getValue() || ''
|
||||
}
|
||||
|
||||
savePatch() {
|
||||
const currentCode = this.editor.getModel()?.getValue() || ''
|
||||
const patch = createTwoFilesPatch(
|
||||
'',
|
||||
'',
|
||||
this.lastCodeValue,
|
||||
currentCode,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
context: 1
|
||||
}
|
||||
)
|
||||
.split('\n')
|
||||
.slice(4)
|
||||
.join('\n')
|
||||
|
||||
this.patches.push(patch)
|
||||
this.lastCodeValue = currentCode
|
||||
if (this.patches.length > MAX_PATCHES) {
|
||||
this.patches.shift()
|
||||
}
|
||||
}
|
||||
|
||||
async predict() {
|
||||
this.reject()
|
||||
await this.autocomplete()
|
||||
this.computeNextPosition()
|
||||
this.displayPrediction()
|
||||
}
|
||||
|
||||
computeNextPosition() {
|
||||
if (this.visualChanges.length > 0) {
|
||||
const position = this.editor.getPosition()
|
||||
if (!position) {
|
||||
return
|
||||
}
|
||||
|
||||
let closestPosition:
|
||||
| {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
| undefined = undefined
|
||||
let closestDistance = Infinity
|
||||
for (const change of this.visualChanges) {
|
||||
if (change.type === 'deleted') {
|
||||
const distance = Math.min(
|
||||
Math.abs(change.range.startLine - position.lineNumber) +
|
||||
Math.abs(change.range.startColumn - position.column) / 10000,
|
||||
Math.abs(change.range.endLine - position.lineNumber) +
|
||||
Math.abs(change.range.endColumn - position.column) / 10000
|
||||
)
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance
|
||||
closestPosition = {
|
||||
lineNumber: change.range.startLine,
|
||||
column: change.range.startColumn
|
||||
}
|
||||
}
|
||||
} else if (change.type === 'added_block') {
|
||||
const distance = Math.abs(change.position.afterLineNumber - position.lineNumber) + 1
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance
|
||||
closestPosition = {
|
||||
lineNumber: change.position.afterLineNumber,
|
||||
column: 10000
|
||||
}
|
||||
}
|
||||
} else if (change.type === 'added_inline') {
|
||||
const distance =
|
||||
Math.abs(change.position.line - position.lineNumber) +
|
||||
Math.abs(change.position.column - position.column) / 10000
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance
|
||||
closestPosition = {
|
||||
lineNumber: change.position.line,
|
||||
column: change.position.column
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.predictedChange = closestPosition
|
||||
? { position: closestPosition, distance: closestDistance }
|
||||
: undefined
|
||||
|
||||
console.log('predictedChange', this.predictedChange, this.visualChanges)
|
||||
}
|
||||
}
|
||||
|
||||
displayPrediction() {
|
||||
if (this.predictedChange) {
|
||||
if (this.predictedChange.distance < 4) {
|
||||
this.predictedChange = undefined
|
||||
this.displayVisualChanges()
|
||||
} else {
|
||||
// display tab icon
|
||||
const el = document.createElement('div')
|
||||
el.textContent = 'TAB'
|
||||
|
||||
Object.assign(el.style, {
|
||||
position: 'relative',
|
||||
background: '#e7e5e4',
|
||||
color: 'black',
|
||||
padding: '4px',
|
||||
fontSize: '10px',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 1000,
|
||||
opacity: 0.8
|
||||
})
|
||||
|
||||
// Create the arrow (pseudo-element trick doesn't work directly via JS,
|
||||
// so we create a separate element to act like the arrow)
|
||||
const arrow = document.createElement('div')
|
||||
Object.assign(arrow.style, {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '-6px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '0',
|
||||
height: '0',
|
||||
borderLeft: '6px solid transparent',
|
||||
borderRight: '6px solid transparent',
|
||||
borderBottom: '6px solid #e7e5e4'
|
||||
})
|
||||
|
||||
// Add arrow to box
|
||||
el.appendChild(arrow)
|
||||
this.tabWidget = {
|
||||
getId: () => 'tab-widget',
|
||||
getDomNode: () => el,
|
||||
getPosition: () => {
|
||||
if (!this.predictedChange) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
position: {
|
||||
lineNumber: this.predictedChange.position.lineNumber,
|
||||
column: this.predictedChange.position.column
|
||||
},
|
||||
preference: [2] // below
|
||||
}
|
||||
},
|
||||
allowEditorOverflow: true
|
||||
}
|
||||
this.editor.addContentWidget(this.tabWidget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async autocomplete() {
|
||||
const position = this.editor.getPosition()
|
||||
if (!position) {
|
||||
return
|
||||
}
|
||||
|
||||
const model = this.editor.getModel()
|
||||
|
||||
if (!model) {
|
||||
return
|
||||
}
|
||||
|
||||
const thisTs = Date.now()
|
||||
this.lastTs = thisTs
|
||||
|
||||
await sleep(200)
|
||||
|
||||
if (model.isDisposed()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (thisTs !== this.lastTs) {
|
||||
return
|
||||
}
|
||||
|
||||
this.abortController?.abort()
|
||||
this.abortController = new AbortController()
|
||||
|
||||
let modifiableEnd = Math.min(model.getLineCount(), position.lineNumber + 7)
|
||||
while (true) {
|
||||
if (modifiableEnd <= position.lineNumber) {
|
||||
break
|
||||
}
|
||||
const line = model.getLineContent(modifiableEnd)
|
||||
if (line.trim().length > 0) {
|
||||
break
|
||||
}
|
||||
modifiableEnd--
|
||||
}
|
||||
|
||||
let modifiableStart = Math.max(1, position.lineNumber - 3)
|
||||
while (true) {
|
||||
if (modifiableStart >= modifiableEnd) {
|
||||
break
|
||||
}
|
||||
const line = model.getLineContent(modifiableStart)
|
||||
if (line.trim().length > 0) {
|
||||
break
|
||||
}
|
||||
modifiableStart++
|
||||
}
|
||||
|
||||
const newCursorLineNumber = Math.max(position.lineNumber, modifiableStart)
|
||||
const newPos = {
|
||||
lineNumber: newCursorLineNumber,
|
||||
column: newCursorLineNumber === position.lineNumber ? position.column : 0
|
||||
}
|
||||
|
||||
this.applyZone = {
|
||||
startLineNumber: modifiableStart,
|
||||
endLineNumber: modifiableEnd
|
||||
}
|
||||
|
||||
const prefix = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: modifiableStart,
|
||||
endColumn: 1
|
||||
})
|
||||
|
||||
const suffix = model.getValueInRange({
|
||||
startLineNumber: modifiableEnd + 1,
|
||||
startColumn: 0,
|
||||
endLineNumber: model.getLineCount(),
|
||||
endColumn: 10000
|
||||
})
|
||||
|
||||
const modifiablePrefix = model.getValueInRange({
|
||||
startLineNumber: modifiableStart,
|
||||
startColumn: 1,
|
||||
endLineNumber: newPos.lineNumber,
|
||||
endColumn: newPos.column
|
||||
})
|
||||
|
||||
const modifiableSuffix = model.getValueInRange({
|
||||
startLineNumber: newPos.lineNumber,
|
||||
startColumn: newPos.column,
|
||||
endLineNumber: modifiableEnd,
|
||||
endColumn: 10000
|
||||
})
|
||||
|
||||
let returnedCode = await autocompleteRequest(
|
||||
{
|
||||
prefix,
|
||||
modifiablePrefix,
|
||||
modifiableSuffix,
|
||||
suffix,
|
||||
language: this.language,
|
||||
scriptLang: this.scriptLang,
|
||||
events: this.patches
|
||||
},
|
||||
this.abortController
|
||||
)
|
||||
|
||||
if (!returnedCode) {
|
||||
return
|
||||
}
|
||||
|
||||
returnedCode = returnedCode.replace('<CURSOR>', '')
|
||||
|
||||
const editableCode = model.getValueInRange({
|
||||
startLineNumber: modifiableStart,
|
||||
startColumn: 1,
|
||||
endLineNumber: modifiableEnd,
|
||||
endColumn: 10000
|
||||
})
|
||||
const numberOfLines = modifiableEnd - modifiableStart + 1
|
||||
|
||||
let completionLines = getLines(returnedCode)
|
||||
|
||||
let finalCompletionLines: string[] = []
|
||||
if (completionLines.length > numberOfLines) {
|
||||
const nextFirstNonEmptyLine = suffix.split('\n').find((line) => line.trim().length > 8)
|
||||
if (nextFirstNonEmptyLine) {
|
||||
for (const line of completionLines) {
|
||||
if (line === nextFirstNonEmptyLine) {
|
||||
break
|
||||
} else {
|
||||
finalCompletionLines.push(line)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finalCompletionLines = completionLines
|
||||
}
|
||||
} else {
|
||||
finalCompletionLines = completionLines
|
||||
}
|
||||
this.modifiedCode = finalCompletionLines.join('\n')
|
||||
|
||||
const changedLines = diffLines(editableCode, this.modifiedCode)
|
||||
|
||||
this.visualChanges = lineChangesToVisualChanges(changedLines, modifiableStart)
|
||||
}
|
||||
|
||||
async displayVisualChanges() {
|
||||
if (this.visualChanges.length > 0) {
|
||||
const { collection, ids } = await displayVisualChanges(
|
||||
'editor-windmill-autocomplete-style',
|
||||
this.editor,
|
||||
this.visualChanges
|
||||
)
|
||||
this.decorationsCollection = collection
|
||||
this.viewZoneIds = ids
|
||||
|
||||
const lastAddChange = this.visualChanges
|
||||
.reverse()
|
||||
.find((c) => c.type === 'added_inline' || c.type === 'added_block')
|
||||
if (lastAddChange) {
|
||||
if (lastAddChange.type === 'added_inline') {
|
||||
this.lastChangePosition = {
|
||||
lineNumber: lastAddChange.position.line,
|
||||
column: lastAddChange.position.column + lastAddChange.value.length
|
||||
}
|
||||
} else if (lastAddChange.type === 'added_block') {
|
||||
this.lastChangePosition = {
|
||||
lineNumber:
|
||||
lastAddChange.position.afterLineNumber + lastAddChange.value.split('\n').length,
|
||||
column: 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
return this.modifiedCode.length > 0
|
||||
}
|
||||
|
||||
accept() {
|
||||
if (this.predictedChange) {
|
||||
this.editor.setPosition(this.predictedChange.position)
|
||||
}
|
||||
|
||||
if (!this.modifiedCode || !this.applyZone) {
|
||||
return
|
||||
}
|
||||
|
||||
this.editor.executeEdits('completion', [
|
||||
{
|
||||
range: {
|
||||
startLineNumber: this.applyZone.startLineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: this.applyZone.endLineNumber,
|
||||
endColumn: 10000
|
||||
},
|
||||
text: this.modifiedCode
|
||||
}
|
||||
])
|
||||
if (this.lastChangePosition) {
|
||||
this.editor.setPosition(this.lastChangePosition)
|
||||
}
|
||||
this.reject()
|
||||
}
|
||||
|
||||
reject() {
|
||||
this.abortController?.abort()
|
||||
this.editor.changeViewZones((acc) => {
|
||||
for (const id of this.viewZoneIds) {
|
||||
acc.removeZone(id)
|
||||
}
|
||||
this.viewZoneIds = []
|
||||
})
|
||||
this.decorationsCollection?.clear()
|
||||
this.modifiedCode = ''
|
||||
setGlobalCSS('editor-windmill-autocomplete-style', '')
|
||||
this.predictedChange = undefined
|
||||
this.tabWidget && this.editor.removeContentWidget(this.tabWidget)
|
||||
this.tabWidget = undefined
|
||||
this.visualChanges = []
|
||||
}
|
||||
}
|
||||
@@ -1,113 +1,42 @@
|
||||
import { codeCompletionLoading, copilotInfo } from '$lib/stores'
|
||||
import { copilotInfo } from '$lib/stores'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
import { getNonStreamingCompletion } from '../lib'
|
||||
import { getFimCompletion } from '../lib'
|
||||
import { getLangContext } from '../chat/script/core'
|
||||
import { type ScriptLang } from '$lib/gen/types.gen'
|
||||
|
||||
const AUTOCOMPLETE_SYSTEM_PROMPT = `You're a code assistant. Your task is to help the user write code by suggesting the next edit for the user.
|
||||
|
||||
As an intelligent code assistant, your role is to analyze what the user has been doing and then to suggest the most likely next modification.
|
||||
|
||||
## Task
|
||||
|
||||
Your task is to rewrite the <EDITABLE_CODE> section of the code I send you to include an edit the user should make.
|
||||
The <CURSOR> tag marks the position of the user's cursor.
|
||||
|
||||
Follow the following criteria.
|
||||
|
||||
### High-level Guidelines
|
||||
|
||||
- Consider the overall intent and direction of the changes
|
||||
- Take into account what the user has been doing
|
||||
- Maintain the code style and formatting conventions of the language used in the file
|
||||
- Your edit suggestions **must** be small and self-contained. Example: if there are two statements that logically need to be added together, suggest them together instead of one by one.
|
||||
|
||||
### Constraints
|
||||
|
||||
- Preserve indentation and braces/parentheses/brackets balance.
|
||||
- Prefer suggesting actual implementations over suggesting placeholders
|
||||
- Dont explain the code, only return the complete <EDITABLE_CODE> section with your edits. DO NOT return any code after the <EDITABLE_CODE> tag.
|
||||
- If there are no useful edits to make, return the the <EDITABLE_CODE> section unmodified, without the <CURSOR> tag.
|
||||
- Never include the <CURSOR> tag in the response.
|
||||
- Never remove line breaks inside the <EDITABLE_CODE> section.`
|
||||
|
||||
const AUTOCOMPLETE_USER_PROMPT = `
|
||||
WINDMILL LANGUAGE CONTEXT:
|
||||
{lang_context}
|
||||
|
||||
<CODE>
|
||||
{prefix}<EDITABLE_CODE>
|
||||
{modifiablePrefix}<CURSOR>{modifiableSuffix}
|
||||
</EDITABLE_CODE>
|
||||
{suffix}</CODE>
|
||||
|
||||
Return the EDITABLE_CODE section in the form \`\`\`{language}
|
||||
<EDITABLE_CODE>
|
||||
...complete editable code section with your modifications
|
||||
</EDITABLE_CODE>
|
||||
\`\`\``
|
||||
|
||||
function postProcessing(response: string) {
|
||||
const code = response.match(/<EDITABLE_CODE>\n?(.*?)\n?<\/EDITABLE_CODE>/s)?.[1]
|
||||
|
||||
if (!code) {
|
||||
throw new Error('No code found in response')
|
||||
}
|
||||
return code
|
||||
}
|
||||
import { getCommentSymbol } from '../utils'
|
||||
|
||||
export async function autocompleteRequest(
|
||||
context: {
|
||||
prefix: string
|
||||
modifiablePrefix: string
|
||||
modifiableSuffix: string
|
||||
suffix: string
|
||||
language: string
|
||||
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
|
||||
events: string[]
|
||||
},
|
||||
abortController: AbortController
|
||||
) {
|
||||
codeCompletionLoading.set(true)
|
||||
const systemPrompt = AUTOCOMPLETE_SYSTEM_PROMPT
|
||||
const userPrompt = AUTOCOMPLETE_USER_PROMPT.replace(
|
||||
'{lang_context}',
|
||||
getLangContext(context.scriptLang)
|
||||
)
|
||||
.replace('{prefix}', context.prefix)
|
||||
.replace('{modifiablePrefix}', context.modifiablePrefix)
|
||||
.replace('{modifiableSuffix}', context.modifiableSuffix)
|
||||
.replace('{suffix}', context.suffix)
|
||||
.replace('{language}', context.language)
|
||||
.replace('{events}', context.events.join('\n\n'))
|
||||
const langContext = getLangContext(context.scriptLang)
|
||||
|
||||
const info = get(copilotInfo)
|
||||
const commentSymbol = getCommentSymbol(context.scriptLang)
|
||||
|
||||
const providerModel = info.codeCompletionModel
|
||||
if (langContext) {
|
||||
const contextLines = langContext.split('\n')
|
||||
const commentedContext = contextLines.map((line) => `${commentSymbol} ${line}`).join('\n')
|
||||
context.prefix = commentedContext + '\n' + context.prefix
|
||||
}
|
||||
|
||||
const providerModel = get(copilotInfo).codeCompletionModel
|
||||
|
||||
if (!providerModel) {
|
||||
throw new Error('No code completion model selected')
|
||||
}
|
||||
|
||||
try {
|
||||
const completion = await getNonStreamingCompletion(
|
||||
[
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
],
|
||||
abortController,
|
||||
{
|
||||
forceModelProvider: providerModel
|
||||
}
|
||||
)
|
||||
const completion = await getFimCompletion(context.prefix, context.suffix, providerModel, abortController)
|
||||
|
||||
return postProcessing(completion)
|
||||
return completion
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
console.log('Could not generate autocomplete', err.message)
|
||||
}
|
||||
} finally {
|
||||
codeCompletionLoading.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { editor as meditor } from 'monaco-editor'
|
||||
|
||||
/**
|
||||
* Unused for now but might be useful for alternative completion diff
|
||||
*/
|
||||
export class DiffEditorWidget {
|
||||
editor: any
|
||||
domNode: HTMLElement
|
||||
diffContainer: HTMLElement
|
||||
diffEditor: meditor.IStandaloneDiffEditor
|
||||
constructor(editor: meditor.IStandaloneCodeEditor, modified: string, lang: string) {
|
||||
this.editor = editor
|
||||
this.domNode = document.createElement('div')
|
||||
|
||||
this.domNode.style.backgroundColor = 'var(--vscode-editor-background)'
|
||||
this.domNode.style.border = '1px solid #ccc'
|
||||
this.domNode.style.zIndex = '1000' // Make sure it's above other elements
|
||||
|
||||
this.diffContainer = document.createElement('div')
|
||||
this.diffContainer.style.width = '100%'
|
||||
this.diffContainer.style.height = '100%'
|
||||
this.diffContainer.style.padding = '0'
|
||||
this.domNode.appendChild(this.diffContainer)
|
||||
|
||||
// Create a diff editor inside the widget
|
||||
this.diffEditor = meditor.createDiffEditor(this.diffContainer, {
|
||||
readOnly: true,
|
||||
automaticLayout: true,
|
||||
lineNumbers: 'off',
|
||||
renderSideBySide: false,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden'
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
folding: false,
|
||||
glyphMargin: false,
|
||||
renderOverviewRuler: false,
|
||||
overviewRulerLanes: 0,
|
||||
renderIndicators: false,
|
||||
lineDecorationsWidth: 5,
|
||||
lightbulb: {
|
||||
enabled: meditor.ShowLightbulbIconMode.Off
|
||||
},
|
||||
lineNumbersMinChars: 0,
|
||||
renderMarginRevertIcon: false
|
||||
})
|
||||
|
||||
const originalModel = meditor.createModel(editor.getValue() || '', lang)
|
||||
|
||||
const modifiedModel = meditor.createModel(modified, lang)
|
||||
|
||||
this.diffEditor.setModel({
|
||||
original: originalModel,
|
||||
modified: modifiedModel
|
||||
})
|
||||
|
||||
function getMaxColumn(model: meditor.ITextModel) {
|
||||
if (!model) return 0
|
||||
|
||||
let maxColumn = 0
|
||||
const totalLines = model.getLineCount()
|
||||
|
||||
for (let line = 1; line <= totalLines; line++) {
|
||||
maxColumn = Math.max(maxColumn, model.getLineMaxColumn(line))
|
||||
}
|
||||
|
||||
return maxColumn
|
||||
}
|
||||
|
||||
const maxOriginal = getMaxColumn(originalModel)
|
||||
const maxModified = getMaxColumn(modifiedModel)
|
||||
const max = Math.max(maxOriginal, maxModified)
|
||||
const width = Math.min(max * 8, 600)
|
||||
this.domNode.style.width = `${width}px`
|
||||
this.diffEditor.onDidUpdateDiff(() => {
|
||||
const originalLineCount = originalModel.getLineCount()
|
||||
|
||||
const changes = this.diffEditor.getLineChanges() || []
|
||||
|
||||
console.log('changes', changes)
|
||||
|
||||
let extraLines = 0
|
||||
|
||||
console.log('original line count', originalLineCount)
|
||||
|
||||
changes.forEach((change) => {
|
||||
if (change.modifiedEndLineNumber) {
|
||||
extraLines += change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1
|
||||
}
|
||||
})
|
||||
|
||||
const lines = originalLineCount + extraLines
|
||||
console.log('lines', lines)
|
||||
this.domNode.style.height = `${lines * 20}px`
|
||||
})
|
||||
// console.log(changes)
|
||||
// console.log('lineCount1', lineCount1)
|
||||
// console.log('lineCount2', lineCount2)
|
||||
}
|
||||
|
||||
layout() {
|
||||
this.diffEditor.layout()
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 'diffEditorWidget'
|
||||
}
|
||||
|
||||
getDomNode() {
|
||||
return this.domNode
|
||||
}
|
||||
|
||||
getPosition() {
|
||||
return {
|
||||
position: {
|
||||
lineNumber: 1,
|
||||
column: 10000
|
||||
},
|
||||
preference: [meditor.ContentWidgetPositionPreference.EXACT]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { AIProvider, AIProviderModel } from '$lib/gen'
|
||||
import {
|
||||
copilotInfo,
|
||||
copilotSessionModel,
|
||||
workspaceStore,
|
||||
type DBSchema,
|
||||
type GraphqlSchema,
|
||||
type SQLSchema
|
||||
@@ -18,6 +19,7 @@ import { get, type Writable } from 'svelte/store'
|
||||
import { OpenAPI, ResourceService, type Script } from '../../gen'
|
||||
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
|
||||
import { formatResourceTypes } from './utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
|
||||
|
||||
@@ -519,11 +521,77 @@ export async function getNonStreamingCompletion(
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
: workspaceAIClients.getOpenaiClient()
|
||||
|
||||
const completion = await openaiClient.chat.completions.create(config, fetchOptions)
|
||||
response = completion.choices?.[0]?.message.content || ''
|
||||
return response
|
||||
}
|
||||
|
||||
const mistralFimResponseSchema = z.object({
|
||||
choices: z.array(
|
||||
z.object({
|
||||
message: z.object({
|
||||
content: z.string().optional()
|
||||
}),
|
||||
finish_reason: z.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
export const FIM_MAX_TOKENS = 256
|
||||
export async function getFimCompletion(
|
||||
prompt: string,
|
||||
suffix: string,
|
||||
providerModel: AIProviderModel,
|
||||
abortController: AbortController
|
||||
): Promise<string | undefined> {
|
||||
const fetchOptions: {
|
||||
signal: AbortSignal
|
||||
headers: Record<string, string>
|
||||
} = {
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'X-Provider': providerModel.provider
|
||||
}
|
||||
}
|
||||
|
||||
const workspace = get(workspaceStore)
|
||||
|
||||
const response = await fetch(
|
||||
`${location.origin}${OpenAPI.BASE}/w/${workspace}/ai/proxy/fim/completions`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
model: providerModel.model,
|
||||
temperature: 0,
|
||||
prompt,
|
||||
suffix,
|
||||
stop: ['\n\n'],
|
||||
max_tokens: FIM_MAX_TOKENS
|
||||
}),
|
||||
...fetchOptions
|
||||
}
|
||||
)
|
||||
|
||||
const body = await response.json()
|
||||
const parsedBody = mistralFimResponseSchema.parse(body)
|
||||
|
||||
const choice = parsedBody.choices[0]
|
||||
|
||||
if (choice) {
|
||||
if (choice.finish_reason === 'length' && choice.message.content) {
|
||||
// take all the lines before the last
|
||||
const lines = choice.message.content.split('\n')
|
||||
const joined = lines.slice(0, -1).join('\n')
|
||||
return joined
|
||||
} else {
|
||||
return choice.message.content || ''
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
import type { Schema, SchemaProperty } from '../../common'
|
||||
|
||||
import type { ResourceType } from '../../gen'
|
||||
import type { ResourceType, ScriptLang } from '../../gen'
|
||||
|
||||
import { capitalize, toCamel } from '$lib/utils'
|
||||
import YAML from 'yaml'
|
||||
|
||||
export const getCommentSymbol = (
|
||||
lang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
|
||||
): string => {
|
||||
switch (lang) {
|
||||
case 'python3':
|
||||
case 'go':
|
||||
case 'bash':
|
||||
case 'powershell':
|
||||
case 'graphql':
|
||||
case 'ansible':
|
||||
case 'nu':
|
||||
return '#'
|
||||
case 'nativets':
|
||||
case 'bun':
|
||||
case 'deno':
|
||||
case 'php':
|
||||
case 'csharp':
|
||||
case 'java':
|
||||
case 'bunnative':
|
||||
return '//'
|
||||
case 'rust':
|
||||
case 'postgresql':
|
||||
case 'mysql':
|
||||
case 'bigquery':
|
||||
case 'snowflake':
|
||||
case 'mssql':
|
||||
case 'oracledb':
|
||||
case 'duckdb':
|
||||
return '--'
|
||||
default:
|
||||
return '//'
|
||||
}
|
||||
}
|
||||
|
||||
export function compile(schema: Schema) {
|
||||
function rec(x: { [name: string]: SchemaProperty }, root = false) {
|
||||
let res = '{\n'
|
||||
|
||||
@@ -235,6 +235,9 @@
|
||||
</div>
|
||||
|
||||
{#if Object.keys(aiProviders).length > 0}
|
||||
{@const autocompleteModels = selectedAiModels.filter(
|
||||
(m) => m.startsWith('codestral-') && !m.startsWith('codestral-embed')
|
||||
)}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-semibold">Settings</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
@@ -256,21 +259,24 @@
|
||||
<Toggle
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
codeCompletionModel = ''
|
||||
codeCompletionModel = autocompleteModels[0] ?? ''
|
||||
} else {
|
||||
codeCompletionModel = undefined
|
||||
}
|
||||
}}
|
||||
checked={codeCompletionModel != undefined}
|
||||
disabled={autocompleteModels.length == 0}
|
||||
options={{
|
||||
right: 'Code completion'
|
||||
right: 'Code completion (Codestral only)',
|
||||
rightTooltip:
|
||||
'We currently only support Mistral Codestral models for code completion.'
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if codeCompletionModel != undefined}
|
||||
<Label label="Code completion model">
|
||||
<ArgEnum
|
||||
enum_={selectedAiModels}
|
||||
enum_={autocompleteModels}
|
||||
bind:value={codeCompletionModel}
|
||||
disabled={false}
|
||||
autofocus={false}
|
||||
@@ -278,9 +284,6 @@
|
||||
valid={true}
|
||||
create={false}
|
||||
/>
|
||||
<p class="text-xs mt-2">
|
||||
We highly recommend using Mistral's Codestral model for code completion.
|
||||
</p>
|
||||
</Label>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user