Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3347494
feat: adds sql query runner ai auto completion
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
17d4827
Merge branch 'main' into feat/sql-auto-completions
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
92aeaeb
fix: catalog monacopilot version and console.logs removed
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
eb72623
fix: remove typescript error
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
ab12ba8
fix: removing endpoint since fetching from orpc
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
3ad7228
fix: handle mount
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
3a2d975
fix: added completion cache to reduce unnecessary api calls
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
c7938f9
Merge branch 'main' into feat/sql-auto-completions
Rudra-Sankha-Sinhamahapatra Dec 12, 2025
6e492be
fix: lint issue during push
Rudra-Sankha-Sinhamahapatra Dec 12, 2025
b782c7e
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Dec 16, 2025
3f9d557
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Dec 16, 2025
167a4b8
refactor: made useEffect as a separate component
Rudra-Sankha-Sinhamahapatra Dec 18, 2025
b9f2149
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Dec 18, 2025
55ec7e4
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Dec 20, 2025
13e9da7
refactor: merge with main
Rudra-Sankha-Sinhamahapatra Dec 23, 2025
a8e8de2
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Dec 24, 2025
72ba4cf
fix: using DatabaseType instead of manually defining
Rudra-Sankha-Sinhamahapatra Dec 24, 2025
c23bbbf
refactor: moved a lot of code from useEffect to outside
Rudra-Sankha-Sinhamahapatra Dec 24, 2025
37f3deb
Merge branch 'feat/sql-auto-completions' of https://github.com/wannab…
letstri Dec 25, 2025
bb01f51
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Dec 25, 2025
d94244f
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Dec 25, 2025
bf83f7e
fix: monaco console errors & added abort prev api call
Rudra-Sankha-Sinhamahapatra Dec 25, 2025
648250c
fix: eslint issue
Rudra-Sankha-Sinhamahapatra Dec 25, 2025
d45f548
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Jan 6, 2026
4880fd0
refactor: update AI tab completion logic
letstri Jan 6, 2026
4ff2090
refactor: improve context generation in database AI completion
letstri Jan 6, 2026
5ddd450
refactor: enhance prompt formatting for SQL code completion assistant
letstri Jan 6, 2026
ccd6157
refactor: replace auth middleware with subscription requirement for c…
letstri Jan 6, 2026
f7a61c3
chore: merge with main
Rudra-Sankha-Sinhamahapatra Jan 7, 2026
af5e1a4
fix: auto completion issue
Rudra-Sankha-Sinhamahapatra Jan 7, 2026
fa6e954
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 8, 2026
ad7ffbb
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 8, 2026
9bf0187
refactor: merge with main
Rudra-Sankha-Sinhamahapatra Jan 10, 2026
1806867
refactor: merge with main
Rudra-Sankha-Sinhamahapatra Jan 15, 2026
4d56616
refactor: merge with main
Rudra-Sankha-Sinhamahapatra Jan 21, 2026
b0f89c3
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Jan 21, 2026
dbcdb68
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Jan 23, 2026
6aea6db
chore(deps): update monacopilot to v1.2.12
Rudra-Sankha-Sinhamahapatra Jan 31, 2026
0dff40b
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 31, 2026
e29d3ce
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Feb 4, 2026
fb58058
fix: lint errors
Rudra-Sankha-Sinhamahapatra Feb 8, 2026
7d994ed
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Feb 9, 2026
d4eabbd
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Feb 14, 2026
b3e4dee
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Feb 16, 2026
1e1fa70
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Feb 16, 2026
05eeb21
chore: change gemini model
letstri Feb 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions apps/api/orpc/routers/ai/code-completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { google } from '@ai-sdk/google'
import { ConnectionType } from '@conar/shared/enums/connection-type'
import { generateText } from 'ai'
import { type } from 'arktype'
import { orpc, requireSubscriptionMiddleware } from '~/orpc'

export const codeCompletion = orpc
.use(requireSubscriptionMiddleware)
.input(type({
context: 'string',
suffix: 'string?',
instruction: 'string?',
fileContent: 'string',
type: type.valueOf(ConnectionType),
schemaContext: 'string?',
}))
.output(type({
completion: 'string',
}))
.handler(async ({ input }) => {
try {
const enhancedPrompt = [
'You are an expert SQL code completion assistant.',
'',
`Database Type: ${input.type}`,
'',
'Database Schema:',
input.schemaContext || '',
'',
'Current Code BEFORE Cursor:',
input.context || '',
'',
'Current Code AFTER Cursor:',
input.suffix || '',
'',
'Full File Content:',
input.fileContent || '',
'',
`Task: ${input.instruction || 'Complete the SQL query'}`,
'',
'General Safety Rules:',
'- Prioritize SELECT queries (read-only) when the intent is ambiguous.',
'- If the user explicitly starts writing a data modification query (INSERT, UPDATE, etc.), complete it validly.',
'- Do NOT generate destructive schema changes (DROP TABLE, TRUNCATE, ALTER) unless explicitly requested in a generic comment.',
'',
'Specific Instructions:',
'- If the context is empty (start of file), return an empty string.',
'- For UUID/CUID: Use generation functions (e.g. gen_random_uuid()) instead of hardcoded strings if possible.',
'- For Foreign Keys: Do NOT plain guess IDs. Use subqueries (e.g. "(SELECT id FROM users ... LIMIT 1)") to make the query robust without needing external tools.',
'- Look at "Current Code AFTER Cursor". Do NOT generate code that is already present there (like closing parenthesis or semicolons). Make your completion join smoothly with the suffix.',
'- Intelligent Spacing: If the code before the cursor is a keyword or identifier (e.g., \'SELECT\', \'FROM\', \'tableName\') and your completion starts with a keyword/identifier, ensure there is a space at the beginning of your completion.',
'',
'Generate ONLY the completion text that should be inserted (or used to replace the last word).',
'- Do NOT wrap in markdown code blocks.',
'- If no completion is needed, return an empty string.',
].join('\n')

const result = await generateText({
model: google('gemini-2.5-flash'),
prompt: enhancedPrompt,
})

let cleanText = result.text.trim()
if (cleanText.startsWith('```')) {
cleanText = cleanText.replace(/^```\w*\s*/, '').replace(/\s*```$/, '')
}

return {
completion: cleanText,
}
}
catch (error) {
console.error('Error in completion handler:', error instanceof Error ? error.message : 'Unknown error')
return {
completion: '',
}
}
})
1 change: 1 addition & 0 deletions apps/api/orpc/routers/ai/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { chat } from './chat'
export { codeCompletion } from './code-completion'
export { enhancePrompt } from './enhance-prompt'
export { filters } from './filters'
export { fixSQL } from './fix-sql'
Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"hono": "catalog:",
"ioredis": "catalog:",
"ioredis-mock": "catalog:",
"monacopilot": "catalog:",
"nanoid": "catalog:",
"pg": "catalog:",
"posthog-node": "catalog:",
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@clickhouse/client": "catalog:",
"@todesktop/runtime": "catalog:",
"electron-store": "catalog:",
"monacopilot": "catalog:",
"mssql": "catalog:",
"mysql2": "catalog:",
"pg": "catalog:"
Expand Down
48 changes: 48 additions & 0 deletions apps/desktop/src/entities/connection/utils/monaco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,51 @@ export function connectionCompletionService(connection: typeof connections.$infe
return [...items, ...keywordItems]
}
}

export async function connectionAICompletionContext(connection: typeof connections.$inferSelect) {
const store = connectionStore(connection.id)
queryClient.prefetchQuery(connectionTablesAndSchemasQuery({ connection, showSystem: store.state.showSystem }))
queryClient.prefetchQuery(connectionEnumsQuery({ connection }))

const buildSchemaContext = async (): Promise<string> => {
const [tablesAndSchemas, enums] = await Promise.all([
queryClient.ensureQueryData(connectionTablesAndSchemasQuery({ connection, showSystem: store.state.showSystem })),
queryClient.ensureQueryData(connectionEnumsQuery({ connection })),
])

const contextLines: string[] = []

for (const schema of tablesAndSchemas.schemas) {
contextLines.push('', `Schema: ${schema.name}`, '')

for (const tableName of schema.tables) {
const columns = await queryClient.ensureQueryData(
connectionTableColumnsQuery({
connection,
schema: schema.name,
table: tableName,
}),
)

contextLines.push(` Table: ${tableName}`, '')
columns.forEach((col) => {
contextLines.push(` - ${col.id}: ${col.type} ${col.isNullable ? '(nullable)' : '(not null)'}`)
})
}
}

if (enums?.length > 0) {
contextLines.push('', 'Enums:', '')
enums.forEach((enumItem) => {
contextLines.push(` ${enumItem.schema}.${enumItem.name}: [${enumItem.values.join(', ')}]`)
})
}

return contextLines.join('\n')
}

return {
type: connection.type,
buildSchemaContext,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import type { ConnectionType } from '@conar/shared/enums/connection-type'
import type { editor } from 'monaco-editor'
import type { CompletionRegistration } from 'monacopilot'
import type { RefObject } from 'react'
import * as monaco from 'monaco-editor'
import { LanguageIdEnum } from 'monaco-sql-languages'
import { registerCompletion } from 'monacopilot'
import { useCallback, useEffect, useRef } from 'react'
import { connectionAICompletionContext } from '~/entities/connection/utils/monaco'
import { orpc } from '~/lib/orpc'
import { Route } from '../..'

const dialectsMap = {
postgres: LanguageIdEnum.PG,
mysql: LanguageIdEnum.MYSQL,
mssql: LanguageIdEnum.PG,
clickhouse: LanguageIdEnum.MYSQL,
} satisfies Record<ConnectionType, LanguageIdEnum>

interface Cache {
before: string
completion: string
}

function getCache(
model: editor.ITextModel,
position: monaco.Position,
cache: Cache | null,
): { completion: string } | null {
if (!cache)
return null

const offset = model.getOffsetAt(position)
const textBefore = model.getValue().substring(0, offset)

if (!textBefore.startsWith(cache.before))
return null

const addedText = textBefore.slice(cache.before.length)

if (!cache.completion.startsWith(addedText))
return null

const remainingCompletion = cache.completion.slice(addedText.length)
return { completion: remainingCompletion }
}

async function fetchCompletion(
model: editor.ITextModel,
position: monaco.Position,
aiConfig: Awaited<ReturnType<typeof connectionAICompletionContext>>,
signal: AbortSignal,
): Promise<{ result: { completion: string }, context: string } | null> {
const offset = model.getOffsetAt(position)
const context = model.getValue().substring(0, offset)
const suffix = model.getValue().substring(offset)

if (context.trim().length < 2)
return null

const schemaContext = await aiConfig.buildSchemaContext()
const result = await orpc.ai.codeCompletion({
context,
suffix,
instruction: 'Complete the SQL query with secure and safe optimised version',
fileContent: model.getValue(),
type: aiConfig.type,
schemaContext,
}, { signal })

return { result, context }
}

export function useRunnerEditorAiTabCompletion(monacoRef: RefObject<editor.IStandaloneCodeEditor | null>) {
const { connection } = Route.useRouteContext()

const completionRef = useRef<CompletionRegistration | null>(null)
const completionCacheRef = useRef<Cache | null>(null)
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const pendingResolveRef = useRef<((value: { completion: string }) => void) | null>(null)
const aiConfigRef = useRef<Awaited<ReturnType<typeof connectionAICompletionContext>> | null>(null)
const abortControllerRef = useRef<AbortController | null>(null)

const clearPending = useCallback(() => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
debounceTimerRef.current = null
}
if (abortControllerRef.current) {
abortControllerRef.current.abort()
abortControllerRef.current = null
}
if (pendingResolveRef.current) {
pendingResolveRef.current({ completion: '' })
pendingResolveRef.current = null
}
}, [])

const requestHandler = useCallback(async () => {
const model = monacoRef.current?.getModel()
const position = monacoRef.current?.getPosition()
const aiConfig = aiConfigRef.current

if (!model || !position || !aiConfig)
return { completion: '' }

const cachedResult = getCache(model, position, completionCacheRef.current)
if (cachedResult)
return cachedResult

if (completionCacheRef.current)
completionCacheRef.current = null

clearPending()

return new Promise<{ completion: string }>((resolve) => {
pendingResolveRef.current = resolve
debounceTimerRef.current = setTimeout(async () => {
if (pendingResolveRef.current !== resolve) {
resolve({ completion: '' })
return
}

const model = monacoRef.current?.getModel()
const position = monacoRef.current?.getPosition()

if (!model || !position || !aiConfigRef.current) {
resolve({ completion: '' })
return
}

const controller = new AbortController()
abortControllerRef.current = controller

try {
const fetchResult = await fetchCompletion(
model,
position,
aiConfigRef.current,
controller.signal,
)

if (!fetchResult) {
resolve({ completion: '' })
return
}

completionCacheRef.current = {
before: fetchResult.context,
completion: fetchResult.result.completion,
}

if (pendingResolveRef.current === resolve) {
resolve({ completion: fetchResult.result.completion })
pendingResolveRef.current = null
}
else {
resolve({ completion: '' })
}
}
catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
resolve({ completion: '' })
return
}
console.error('AI completion error:', error)
resolve({ completion: '' })
}
finally {
if (pendingResolveRef.current === resolve)
pendingResolveRef.current = null
}
}, 600)
})
}, [clearPending, monacoRef])

useEffect(() => {
let isMounted = true

const setupCompletion = async () => {
if (!monacoRef.current)
return

const config = await connectionAICompletionContext(connection)
if (!isMounted)
return
aiConfigRef.current = config

if (completionRef.current) {
completionRef.current.deregister()
}

// Monkey patch to fix 'disposeInlineCompletions is not a function' error from monacopilot
const originalRegister = monaco.languages.registerInlineCompletionsProvider
// @ts-expect-error - patching internal api
if (!originalRegister.__patched) {
monaco.languages.registerInlineCompletionsProvider = (languageSelector, provider) => {
if (!provider.disposeInlineCompletions) {
// eslint-disable-next-line ts/ban-ts-comment
// @ts-ignore
provider.disposeInlineCompletions = () => {}
}
return originalRegister.call(monaco.languages, languageSelector, provider)
}
// @ts-expect-error - patching internal api
monaco.languages.registerInlineCompletionsProvider.__patched = true
}

completionRef.current = registerCompletion(
monaco,
monacoRef.current,
{
trigger: 'onTyping',
language: dialectsMap[connection.type],
requestHandler,
},
)
}

setupCompletion()

return () => {
isMounted = false
clearPending()
if (completionRef.current) {
completionRef.current.deregister()
completionRef.current = null
}
}
}, [clearPending, connection, monacoRef, requestHandler])
}
Loading