@molecule/app-ai-copilot-default
Provider bond · ai-copilot · App (browser) · v1.0.2 · Apache-2.0
Default HTTP/SSE copilot provider for inline AI suggestions
npm install @molecule/app-ai-copilot-defaultnpm · Source on GitHub · Implements @molecule/app-ai-copilot
How it works
@molecule/app-ai-copilot-default is a provider bond on the app (browser) side: it implements the ai-copilot core interface (@molecule/app-ai-copilot) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setProvider } from '@molecule/app-ai-copilot'
import { provider } from '@molecule/app-ai-copilot-default'
setProvider(provider) // at startup — lazy; same-origin base URL, no config needed
// setProvider(createProvider({ baseUrl, headers })) to customizeWorks with: @molecule/app-ai-copilot, @molecule/app-i18n
Live demos
Running apps built with Molecule that install this package — open the app or study the template.
Reference
Default ai-copilot provider for molecule.dev — HTTP/SSE inline AI suggestions from YOUR backend.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setProvider } from '@molecule/app-ai-copilot'
import { provider } from '@molecule/app-ai-copilot-default'
setProvider(provider) // at startup — lazy; same-origin base URL, no config needed
// setProvider(createProvider({ baseUrl, headers })) to customize
Type
provider
Installation
npm install @molecule/app-ai-copilot-default @molecule/app-ai-copilot @molecule/app-i18n
API
Interfaces
DefaultCopilotConfig
Configuration for the default HTTP-based copilot provider.
interface DefaultCopilotConfig {
/** Base URL for API requests. Defaults to `''` (same origin). */
baseUrl?: string
/** Custom headers to include in every request. */
headers?: Record<string, string>
}
Classes
DefaultCopilotProvider
HTTP/SSE-based copilot provider. Sends document context via POST and reads SSE streams for real-time inline suggestions.
class DefaultCopilotProvider implements AICopilotProvider {
/** @inheritdoc */
readonly name = 'default'
private config: DefaultCopilotConfig
private abortController: AbortController | null = null
/**
* Creates a new DefaultCopilotProvider.
*
* @param config - Optional provider-level configuration (base URL, headers).
*/
constructor(config: DefaultCopilotConfig = {}) {
this.config = config
}
/**
* Requests inline suggestions from the backend.
*
* Aborts any previous in-flight request before starting a new one.
* Parses the SSE stream and emits `CopilotEvent` objects via `onEvent`.
*
* @param context - Current editor context (prefix, suffix, language, etc.).
* @param config - Request-level settings (endpoint, model, maxSuggestions).
* @param onEvent - Callback for each streamed event.
*/
async getSuggestions(
context: CopilotContext,
config: AICopilotConfig,
onEvent: CopilotEventHandler,
): Promise<void> {
this.abortController?.abort()
const controller = new AbortController()
this.abortController = controller
const { signal } = controller
const url = `${this.config.baseUrl ?? ''}${config.endpoint}`
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify({
prefix: context.prefix,
suffix: context.suffix,
language: context.language,
filePath: context.filePath,
cursorLine: context.cursorLine,
cursorColumn: context.cursorColumn,
model: config.model,
maxSuggestions: config.maxSuggestions,
projectId: config.projectId,
}),
signal,
})
if (!response.ok) {
const text = await response
.text()
.catch(() =>
t('copilot.error.unknownError', undefined, { defaultValue: 'Unknown error' }),
)
let errorMessage: string | undefined
try {
const body = JSON.parse(text) as Record<string, unknown>
if (typeof body.error === 'string') errorMessage = body.error
} catch (_error) {
// Not JSON — use raw text
}
onEvent({
type: 'error',
message:
errorMessage ??
t(
'copilot.error.httpError',
{ status: response.status, text },
{ defaultValue: 'HTTP {{status}}: {{text}}' },
),
})
return
}
if (!response.body) {
onEvent({
type: 'error',
message: t('copilot.error.noResponseBody', undefined, {
defaultValue: 'No response body',
}),
})
return
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const data = line.slice(6).trim()
if (!data) continue
try {
const event = JSON.parse(data) as CopilotEvent
onEvent(event)
} catch (_error) {
// Skip malformed SSE data lines
}
}
// Yield to main thread between chunks
await new Promise((r) => setTimeout(r, 0))
}
// Flush any remaining buffered data
if (buffer.startsWith('data: ')) {
const data = buffer.slice(6).trim()
if (data) {
try {
const event = JSON.parse(data) as CopilotEvent
onEvent(event)
} catch (_error) {
// Skip malformed data
}
}
}
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return
}
const message =
err instanceof Error
? err.message
: t('copilot.error.streamError', undefined, { defaultValue: 'Stream error' })
onEvent({ type: 'error', message })
} finally {
if (this.abortController === controller) {
this.abortController = null
}
}
}
/**
* Reports a suggestion acceptance to the backend for analytics/learning.
*
* @param suggestion - The suggestion the user accepted.
* @param config - Request-level configuration (endpoint).
*/
async acceptSuggestion(suggestion: CopilotSuggestion, config: AICopilotConfig): Promise<void> {
const url = `${this.config.baseUrl ?? ''}${config.endpoint}/feedback`
try {
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify({
suggestionId: suggestion.id,
action: 'accept',
text: suggestion.text,
metadata: suggestion.metadata,
}),
})
} catch (_error) {
// Feedback is best-effort — do not propagate errors
}
}
/**
* Reports a suggestion rejection to the backend for analytics/learning.
*
* @param suggestion - The suggestion the user dismissed.
* @param config - Request-level configuration (endpoint).
*/
async rejectSuggestion(suggestion: CopilotSuggestion, config: AICopilotConfig): Promise<void> {
const url = `${this.config.baseUrl ?? ''}${config.endpoint}/feedback`
try {
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify({
suggestionId: suggestion.id,
action: 'reject',
metadata: suggestion.metadata,
}),
})
} catch (_error) {
// Feedback is best-effort — do not propagate errors
}
}
/**
* Cancels any in-flight suggestion request.
*/
abort(): void {
this.abortController?.abort()
this.abortController = null
}
}
Functions
createProvider(config)
Creates a DefaultCopilotProvider instance.
function createProvider(config?: DefaultCopilotConfig): DefaultCopilotProvider
config— Optional provider-level configuration (base URL, headers).
Returns: A new DefaultCopilotProvider.
Constants
provider
The provider implementation — the fleet-standard typed provider const.
Wire it once at startup: setProvider(provider) from @molecule/app-ai-copilot.
It is a lazy proxy: construction is deferred to the first property access, so
importing this module never throws and needs no config up front. Use
createProvider(config) instead when you need to pass a base URL or headers.
const provider: AICopilotProvider
Core Interface
Implements @molecule/app-ai-copilot interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/app-ai-copilot'
import { provider } from '@molecule/app-ai-copilot-default'
export function setupAiCopilotDefault(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/app-ai-copilot^1.0.1@molecule/app-i18n^1.0.1
Runtime Dependencies
@molecule/app-ai-copilot@molecule/app-i18n
Server contract: getSuggestions POSTs { prefix, suffix, language, filePath?, cursorLine?, cursorColumn?, model?, maxSuggestions?, projectId? } to config.endpoint and reads an SSE stream of
data: <CopilotEvent JSON> lines. acceptSuggestion /
rejectSuggestion POST { suggestionId, action: 'accept' | 'reject', text?, metadata } to ${config.endpoint}/feedback — best-effort, errors
are swallowed, so implement the route (or expect silent no-ops).
getSuggestions auto-aborts the previous in-flight request; still call
abort() on keystrokes you debounce away (see @molecule/app-ai-copilot).
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- Typing in the editor fires
getSuggestions(context, config, onEvent)and the returnedCopilotSuggestion.textrenders as ghost/inline text anchored to the suggestion'srange(CopilotRange) — or the caret whenrangeis omitted — never at a stale or wrong offset. - Accepting the suggestion (e.g. Tab) inserts EXACTLY
suggestion.textat thatrange/caret and nothing stale, and firesacceptSuggestion(suggestion, config); the buffer holds only the accepted text with no leftover ghost preview. - Continuing to type or explicitly dismissing removes the ghost cleanly
and calls
abort()so the in-flight request is cancelled before the nextgetSuggestions— a late-arriving stale suggestion never lands at the moved cursor, andrejectSuggestion(suggestion, config)reports the miss. - The suggestion is context-aware, not generic: it reflects the real
CopilotContext(prefix/suffix/languagearound the cursor), so editing the surrounding code visibly changes what gets proposed. - With copilot disabled (no provider bonded, or the app's config/toggle off) no ghost text ever appears and typing stays completely unaffected.
- A provider error (an
onEvent{ type: 'error' }) fails quietly — no ghost text, no thrown exception in the editor, the buffer is untouched, and the user can keep typing. - Correctness/security: accepted text is inserted ONLY at the intended
CopilotRange(it never overwrites unrelated lines), andsuggestion.textis treated as plain model output — inserted as text, never eval'd or run as trusted code by the copilot itself.