diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a051bd..9133e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documente ### Added +- **`[Autocomplete]` Chat-prompt suggestions are now opt-in.** VS Code exposes the Copilot Chat prompt box as a virtual `chatSessionInput` document, and the `"**"` provider selector matched it — so ghost text appeared while writing prompts. The provider now only serves real editable code surfaces (`isCompletionDocument` — a code-editor scheme allowlist, so future interactive surfaces are excluded automatically) with an opt-in carve-out for the chat prompt via `opencodego.inlineSuggestionsChatInput`. + - **`[Autocomplete]` Known limitation (follow-up planned):** inline-completion requests are not yet wired into the Go usage tracker's cost accounting (`tracker.record()` only runs in the chat provider path). The panel does track **Suggested / Approved** counts per day (see the usage dashboard entry), but the USD cost of completions is not attributed until a follow-up ships the transport summary from the completion engine. This is tracked as a documented TODO on #136/#138 rather than an oversight. - **`[Autocomplete]` Inline code suggestions (experimental, #49).** Ghost-text completions while typing, powered by the OpenCode gateway with thinking forced off. Opt-in via `opencodego.inlineSuggestions` (default `false`); model via `opencodego.inlineSuggestionsModel` (default `qwen3.5-plus`, whose `enable_thinking=false` mode is a genuine no-reasoning path — measured ~1.5s time-to-first-token with zero hidden reasoning). Requests are tiny (10 lines before the cursor + a short suffix), debounced 300ms, time out at 3s and abort on the next keystroke. The gateway exposes no FIM endpoint, so completions emulate fill-in-the-middle with FIM tokens over `/chat/completions`. New `src/autocomplete/` module (context, prompt, throttle, engine, provider, registration) with unit tests; `scripts/probe-completion-latency.ts` measures engine latency live. All timing/size knobs are user-tunable: `inlineSuggestionsDebounceMs`, `inlineSuggestionsTimeoutMs`, `inlineSuggestionsMaxTokens`, `inlineSuggestionsPrefixLines`, `inlineSuggestionsSuffixChars`. diff --git a/package.json b/package.json index b228d1c..0d4451a 100644 --- a/package.json +++ b/package.json @@ -300,6 +300,11 @@ "default": "qwen3.5-plus", "markdownDescription": "Model used for inline code suggestions. Prefer the non-thinking Qwen options — measured: `qwen3.5-plus` with `enable_thinking=false` returns in ~1.5s with zero hidden reasoning, while reasoning models (e.g. `deepseek-v4-flash`) burn 100+ reasoning tokens even with thinking off." }, + "opencodego.inlineSuggestionsChatInput": { + "type": "boolean", + "default": false, + "description": "Also offer inline suggestions inside the GitHub Copilot Chat prompt box while typing prompts. Off by default — completions are meant for code editors." + }, "opencodego.inlineSuggestionsDebounceMs": { "type": "number", "default": 300, diff --git a/src/autocomplete/context.ts b/src/autocomplete/context.ts index 292671a..5db3c70 100644 --- a/src/autocomplete/context.ts +++ b/src/autocomplete/context.ts @@ -15,6 +15,35 @@ export { DEFAULT_INLINE_MAX_TOKENS as DEFAULT_MAX_TOKENS, } from "../config"; +/** + * VS Code exposes the Copilot Chat prompt box as a virtual editor document + * with one of these schemes (see `chatInputSchemes` in the VS Code source: + * `workbench/contrib/chat/common/constants.ts`). Inline-completion providers + * registered on `"**"` are asked for suggestions there too — ghost text must + * never appear while the user is typing a prompt. + */ +export const CHAT_INPUT_SCHEMES = new Set(["chatSessionInput", "sessions-chat"]); + +/** Whether a document is a chat/interactive prompt box (no completions there). */ +export function isChatInputDocument(uri: { scheme: string }): boolean { + return CHAT_INPUT_SCHEMES.has(uri.scheme); +} + +/** + * Document schemes that are real editable code surfaces. Inline completions + * are only offered there. An ALLOWLIST (instead of only blocking known chat + * schemes) means any new interactive surface VS Code introduces in the + * future — chat prompt variants, webviews, output, custom editors — is + * excluded automatically without a manual update; genuinely new CODE + * surfaces are rare. + */ +export const CODE_EDITOR_SCHEMES = new Set(["file", "untitled", "git", "vscode-userdata", "vscode-notebook-cell"]); + +/** Whether a document is a normal editable code surface. */ +export function isCompletionDocument(uri: { scheme: string }): boolean { + return CODE_EDITOR_SCHEMES.has(uri.scheme); +} + export interface CompletionWindowOptions { prefixLines?: number; suffixChars?: number; diff --git a/src/autocomplete/index.ts b/src/autocomplete/index.ts index 13b9a9b..01a93ce 100644 --- a/src/autocomplete/index.ts +++ b/src/autocomplete/index.ts @@ -15,6 +15,7 @@ import { COMPLETION_USAGE_KEY, COMPLETION_USAGE_MAX_DAYS, CONFIG_SECTION, + DEFAULT_INLINE_SUGGESTIONS_CHAT_INPUT, DEFAULT_INLINE_DEBOUNCE_MS, DEFAULT_INLINE_MAX_TOKENS, DEFAULT_INLINE_MODEL, @@ -28,6 +29,7 @@ import { INLINE_SUGGESTIONS_SETTING, INLINE_SUFFIX_CHARS_SETTING, INLINE_TIMEOUT_MS_SETTING, + SETTING_INLINE_SUGGESTIONS_CHAT_INPUT, } from "../config"; import { toFiniteNumber } from "../utils"; import { bumpCompletionUsage, matchesAcceptance, utcDayStart, type CompletionUsageDay } from "./usage"; @@ -146,6 +148,7 @@ export function registerInlineCompletions(context: vscode.ExtensionContext, deps resolveMaxTokens: () => readNumberSetting(INLINE_MAX_TOKENS_SETTING, DEFAULT_INLINE_MAX_TOKENS, 16, 1_024), resolvePrefixLines: () => readNumberSetting(INLINE_PREFIX_LINES_SETTING, DEFAULT_INLINE_PREFIX_LINES, 1, 100), resolveSuffixChars: () => readNumberSetting(INLINE_SUFFIX_CHARS_SETTING, DEFAULT_INLINE_SUFFIX_CHARS, 0, 5_000), + resolveChatInputEnabled: () => readSetting(SETTING_INLINE_SUGGESTIONS_CHAT_INPUT, DEFAULT_INLINE_SUGGESTIONS_CHAT_INPUT), }); const registration = vscode.languages.registerInlineCompletionItemProvider({ pattern: "**" }, provider); diff --git a/src/autocomplete/provider.ts b/src/autocomplete/provider.ts index 82b6382..1119408 100644 --- a/src/autocomplete/provider.ts +++ b/src/autocomplete/provider.ts @@ -7,7 +7,7 @@ */ import * as vscode from "vscode"; -import { buildCompletionWindow } from "./context"; +import { buildCompletionWindow, isChatInputDocument, isCompletionDocument } from "./context"; import { Debouncer } from "./throttle"; import type { CompletionContext, CompletionEngine } from "./types"; @@ -21,6 +21,8 @@ export interface InlineCompletionProviderOptions { isEnabled: () => boolean; /** The model to use for suggestions (config-driven). */ resolveModelId: () => string; + /** Whether suggestions are allowed inside the chat prompt box (opt-in). */ + resolveChatInputEnabled: () => boolean; /** Debounce delay in ms before a request is sent (config-driven). */ resolveDebounceMs: () => number; /** Max tokens a completion may produce (config-driven). */ @@ -48,6 +50,14 @@ export class OpenCodeInlineCompletionProvider implements vscode.InlineCompletion return Promise.resolve(undefined); } + // Only offer completions in real editable code surfaces. The Copilot + // Chat prompt box is a virtual chatSessionInput document — excluded by + // default (completions belong in code editors); users can opt in via + // opencodego.inlineSuggestionsChatInput. + if (!isCompletionDocument(document.uri) && !(isChatInputDocument(document.uri) && this.options.resolveChatInputEnabled())) { + return Promise.resolve(undefined); + } + // Keep the debounce window live: a config change applies on the next // keystroke instead of requiring the provider to be recreated. const debounceMs = this.options.resolveDebounceMs(); diff --git a/src/config.ts b/src/config.ts index f0ca714..f4680b7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -56,6 +56,9 @@ export const SETTING_THINKING_MIMO = "thinking.mimo"; export const INLINE_SUGGESTIONS_SETTING = "inlineSuggestions"; export const INLINE_SUGGESTIONS_MODEL_SETTING = "inlineSuggestionsModel"; +/** Opt-in: also offer completions inside the Copilot Chat prompt box. */ +export const SETTING_INLINE_SUGGESTIONS_CHAT_INPUT = "inlineSuggestionsChatInput"; +export const DEFAULT_INLINE_SUGGESTIONS_CHAT_INPUT = false; export const INLINE_DEBOUNCE_MS_SETTING = "inlineSuggestionsDebounceMs"; export const INLINE_TIMEOUT_MS_SETTING = "inlineSuggestionsTimeoutMs"; export const INLINE_MAX_TOKENS_SETTING = "inlineSuggestionsMaxTokens"; diff --git a/src/test/autocomplete.test.ts b/src/test/autocomplete.test.ts index 57f5e79..139e550 100644 --- a/src/test/autocomplete.test.ts +++ b/src/test/autocomplete.test.ts @@ -1,6 +1,12 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import { buildCompletionWindow, DEFAULT_PREFIX_LINES, DEFAULT_SUFFIX_CHARS } from "../autocomplete/context"; +import { + buildCompletionWindow, + DEFAULT_PREFIX_LINES, + DEFAULT_SUFFIX_CHARS, + isChatInputDocument, + isCompletionDocument, +} from "../autocomplete/context"; import { buildCompletionPrompt, completionFamily, COMPLETION_SYSTEM_PROMPT } from "../autocomplete/prompt"; import { cleanCompletion, extractChatCompletionText, parseSseData } from "../autocomplete/engine"; import { Debouncer } from "../autocomplete/throttle"; @@ -103,6 +109,36 @@ describe("autocomplete — engine parsing", () => { }); }); +describe("autocomplete — isChatInputDocument", () => { + it("rejects the Copilot Chat prompt box schemes", () => { + assert.ok(isChatInputDocument({ scheme: "chatSessionInput" })); + assert.ok(isChatInputDocument({ scheme: "sessions-chat" })); + }); + + it("accepts real code documents", () => { + assert.ok(!isChatInputDocument({ scheme: "file" })); + assert.ok(!isChatInputDocument({ scheme: "untitled" })); + }); +}); + +describe("autocomplete — isCompletionDocument (code-editor allowlist)", () => { + it("accepts real editable code surfaces", () => { + assert.ok(isCompletionDocument({ scheme: "file" })); + assert.ok(isCompletionDocument({ scheme: "untitled" })); + assert.ok(isCompletionDocument({ scheme: "git" })); + assert.ok(isCompletionDocument({ scheme: "vscode-userdata" })); + assert.ok(isCompletionDocument({ scheme: "vscode-notebook-cell" })); + }); + + it("excludes chat prompts and other non-code surfaces", () => { + assert.ok(!isCompletionDocument({ scheme: "chatSessionInput" })); + assert.ok(!isCompletionDocument({ scheme: "sessions-chat" })); + assert.ok(!isCompletionDocument({ scheme: "output" })); + assert.ok(!isCompletionDocument({ scheme: "webviewPanel" })); + assert.ok(!isCompletionDocument({ scheme: "vscode-interactive-input" })); + }); +}); + describe("autocomplete — Debouncer", () => { it("honors a custom delay", async () => { const d = new Debouncer(120);