3 Commits
Author SHA1 Message Date
HarvmasterandClaude Opus 5.5 9ccf108835 Extension: request the next edit right after accepting a suggestion (0.6.4)
ci / build (push) Canceled after 0s
Accepting a completion or a next-edit cancels the debounce timer and asks the daemon for
the follow-up immediately; next-edits chain. Setting localAutocomplete.nextEdit.afterAccept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J39imRDT8L7xYbWzsVWd8A
2026-10-04 01:11:39 +00:00
HarvmasterandClaude Opus 5.5 3dc916834f Daemon: a changed default model overrides the saved state (0.6.3)
ci / build (push) Canceled after 0s
In managed mode the extension switches models by rewriting the daemon config's
defaultModel and restarting it, but loadState() preferred the persisted model, so the
daemon came back on the old one every time. State now records the default it was saved
under; a different default wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J39imRDT8L7xYbWzsVWd8A
2026-10-04 01:06:31 +00:00
HarvmasterandClaude Opus 5.5 01e778daf6 Next-edit: n-gram speculative decoding on the companion server (0.6.2)
ci / build (push) Canceled after 0s
The rewritten region is mostly a copy of the current one, so prompt-lookup drafts are
accepted in long runs. Greedy decoding keeps the output identical. Laptop CPU, 160-token
region rewrite: generation 1.76 s -> 0.37 s. Timings now include errorsMs/promptMs/genMs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J39imRDT8L7xYbWzsVWd8A
2026-10-04 00:54:49 +00:00
5 changed files with 50 additions and 6 deletions
+19
View File
@@ -2,6 +2,25 @@
Extension versions are released as `vscode-v<version>` on the [Releases](https://git.harvmaster.com/Harvmaster/autocomplete-model/releases) page. Models are versioned by their name in [`models/manifest.json`](models/manifest.json). Extension versions are released as `vscode-v<version>` on the [Releases](https://git.harvmaster.com/Harvmaster/autocomplete-model/releases) page. Models are versioned by their name in [`models/manifest.json`](models/manifest.json).
## vscode 0.6.4
- **Accepting a suggestion asks for the next edit immediately** (setting `nextEdit.afterAccept`, on by default). After
Tab on a completion or a next-edit, the follow-up is requested right away instead of after the typing pause, so
accepted next-edits chain (rename → next use → …).
## vscode 0.6.3
- **Fix: model choice didn't stick in managed mode.** The daemon's saved state (last model used) overrode both the
model picked in the menu and a new default from a release, so it kept restarting on the old model (e.g.
`stage-c`). A changed default now wins; switching through the daemon API still persists as before.
## vscode 0.6.2
- **Next-edit is ~3–5× faster:** the next-edit server uses n-gram speculative decoding (the rewritten region is mostly
a copy of the current one, so drafted tokens are verified in batches). Same output, since decoding is greedy.
Rewriting a 13-line region on a laptop CPU: ~2.1 s → ~0.7 s (~0.4 s when the prompt is cached).
- Next-edit timings in "Show Last Prompt" now split TS errors / prompt / generation time.
## vscode 0.6.1 ## vscode 0.6.1
- **New default model `stage-d`**, with **`stage-e` for next-edit**. Both download automatically (~530 MB each); the - **New default model `stage-d`**, with **`stage-e` for next-edit**. Both download automatically (~530 MB each); the
+9 -3
View File
@@ -32,6 +32,9 @@ export interface DaemonConfig {
ctx: number ctx: number
/** Extra flags, e.g. ["-ngl", "99"] on Apple Silicon, ["-t", "16"] on the laptop CPU. */ /** Extra flags, e.g. ["-ngl", "99"] on Apple Silicon, ["-t", "16"] on the laptop CPU. */
args: string[] args: string[]
/** Extra flags for the next-edit companion server only. Default: n-gram speculative decoding (the output is
* mostly a copy of the region, so drafts from the prompt are accepted; exact with greedy sampling). */
nextEditArgs?: string[]
} }
models: Record<string, ModelProfile> models: Record<string, ModelProfile>
defaultModel: string defaultModel: string
@@ -73,6 +76,8 @@ export interface RuntimeState {
enabled: boolean enabled: boolean
model: string model: string
mode: ContextMode mode: ContextMode
/** config.defaultModel when this state was saved: a changed default (editor model setting, new release) wins. */
defaultModel?: string
} }
export function loadConfig(configPath: string): DaemonConfig { export function loadConfig(configPath: string): DaemonConfig {
@@ -94,12 +99,13 @@ export function loadConfig(configPath: string): DaemonConfig {
} }
export function loadState(config: DaemonConfig): RuntimeState { export function loadState(config: DaemonConfig): RuntimeState {
const fallback: RuntimeState = { enabled: true, model: config.defaultModel, mode: config.models[config.defaultModel].defaultMode } const fallback: RuntimeState = { enabled: true, model: config.defaultModel, mode: config.models[config.defaultModel].defaultMode, defaultModel: config.defaultModel }
try { try {
const s = JSON.parse(fs.readFileSync(config.stateFile, 'utf8')) as Partial<RuntimeState> const s = JSON.parse(fs.readFileSync(config.stateFile, 'utf8')) as Partial<RuntimeState>
const model = s.model && config.models[s.model] ? s.model : fallback.model const keepModel = s.defaultModel === config.defaultModel && s.model && config.models[s.model]
const model = keepModel ? s.model! : fallback.model
const mode = s.mode && config.models[model].modes.includes(s.mode) ? s.mode : config.models[model].defaultMode const mode = s.mode && config.models[model].modes.includes(s.mode) ? s.mode : config.models[model].defaultMode
return { enabled: s.enabled ?? true, model, mode } return { enabled: s.enabled ?? true, model, mode, defaultModel: config.defaultModel }
} catch { } catch {
return fallback return fallback
} }
+5 -2
View File
@@ -52,7 +52,8 @@ function loadModel(name: string) {
} }
// Next-edit companion model (profile.nextEditModel): a second llama-server, started on first use. // Next-edit companion model (profile.nextEditModel): a second llama-server, started on first use.
const neLlama = new LlamaServer({ ...config.llamaServer, port: config.llamaServer.port + 1 }) const NEXT_EDIT_SPECULATION = ['--spec-type', 'ngram-simple', '--spec-draft-n-max', '64', '--spec-ngram-simple-size-n', '3']
const neLlama = new LlamaServer({ ...config.llamaServer, port: config.llamaServer.port + 1, args: [...config.llamaServer.args, ...(config.llamaServer.nextEditArgs ?? NEXT_EDIT_SPECULATION)] })
let neLoading: Promise<void> | null = null let neLoading: Promise<void> | null = null
/** The server that answers next-edit for the active model, or an error message. */ /** The server that answers next-edit for the active model, or an error message. */
async function nextEditServer(): Promise<LlamaServer | string> { async function nextEditServer(): Promise<LlamaServer | string> {
@@ -273,9 +274,11 @@ async function handleNextEdit(body: NextEditRequest, res: http.ServerResponse, c
const endLine = Math.min(lines.length, cursorLine + o.regionAfter + 1) const endLine = Math.min(lines.length, cursorLine + o.regionAfter + 1)
const recentEdits = edits.observe(`${body.workspaceRoot ?? body.repo}/${body.path}`, body.path, body.text, cursorLine) const recentEdits = edits.observe(`${body.workspaceRoot ?? body.repo}/${body.path}`, body.path, body.text, cursorLine)
const lang = symbolLang(body.languageId ?? '') const lang = symbolLang(body.languageId ?? '')
const tErrors = performance.now()
const errors = body.workspaceRoot && lang const errors = body.workspaceRoot && lang
? await symbols.errors({ root: body.workspaceRoot, filePath: path.join(body.workspaceRoot, body.path), lang, text: body.text, cursor: body.cursor, waitMs: 300 }) ? await symbols.errors({ root: body.workspaceRoot, filePath: path.join(body.workspaceRoot, body.path), lang, text: body.text, cursor: body.cursor, waitMs: 300 })
: undefined : undefined
const errorsMs = performance.now() - tErrors
const ids = builder.buildNextEdit({ repo: body.repo, path: body.path, file: body.text, regionStartLine: startLine, regionEndLine: endLine, cursor: body.cursor, recentEdits: recentEdits || undefined, errors }, const ids = builder.buildNextEdit({ repo: body.repo, path: body.path, file: body.text, regionStartLine: startLine, regionEndLine: endLine, cursor: body.cursor, recentEdits: recentEdits || undefined, errors },
{ beforeTokens: o.beforeTokens, afterTokens: o.afterTokens }) { beforeTokens: o.beforeTokens, afterTokens: o.afterTokens })
@@ -294,7 +297,7 @@ async function handleNextEdit(body: NextEditRequest, res: http.ServerResponse, c
const response = { const response = {
requestId: body.requestId, noEdit, startLine, endLine, oldText, newText: noEdit ? null : newText, requestId: body.requestId, noEdit, startLine, endLine, oldText, newText: noEdit ? null : newText,
recentEdits, errors: errors ?? null, model: profile.nextEditModel ?? state.model, recentEdits, errors: errors ?? null, model: profile.nextEditModel ?? state.model,
timings: { promptTokens: ids.length, processedTokens: result.timings?.prompt_n ?? null, genTokens: result.timings?.predicted_n ?? null, totalMs: round(performance.now() - t0) }, timings: { promptTokens: ids.length, processedTokens: result.timings?.prompt_n ?? null, genTokens: result.timings?.predicted_n ?? null, errorsMs: round(errorsMs), promptMs: round(result.timings?.prompt_ms ?? 0), genMs: round(result.timings?.predicted_ms ?? 0), totalMs: round(performance.now() - t0) },
} }
remember({ kind: 'next-edit', requestId: body.requestId, path: body.path, model: state.model, prompt: builder.decode(ids), raw, noEdit, timings: response.timings }) remember({ kind: 'next-edit', requestId: body.requestId, path: body.path, model: state.model, prompt: builder.decode(ids), raw, noEdit, timings: response.timings })
json(res, 200, response) json(res, 200, response)
+6 -1
View File
@@ -3,7 +3,7 @@
"displayName": "Local Autocomplete", "displayName": "Local Autocomplete",
"description": "Inline code completion from a local fine-tuned model via the acd daemon", "description": "Inline code completion from a local fine-tuned model via the acd daemon",
"publisher": "harvmaster", "publisher": "harvmaster",
"version": "0.6.1", "version": "0.6.4",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"vscode": "^1.90.0" "vscode": "^1.90.0"
@@ -218,6 +218,11 @@
"default": 700, "default": 700,
"description": "Pause after typing before asking for a next-edit suggestion." "description": "Pause after typing before asking for a next-edit suggestion."
}, },
"localAutocomplete.nextEdit.afterAccept": {
"type": "boolean",
"default": true,
"description": "After you accept a completion or a next-edit suggestion, immediately ask for the next edit (without waiting for the typing pause). Accepted next-edits chain: a rename suggests the next use, and so on."
},
"localAutocomplete.triggerOnCursorMove": { "localAutocomplete.triggerOnCursorMove": {
"type": "boolean", "type": "boolean",
"default": false, "default": false,
+11
View File
@@ -259,6 +259,7 @@ const modelSupportsNextEdit = () => !!(daemonConfig && daemonConfig.models[daemo
// ---------- next-edit prediction (models trained for it, e.g. Stage E) // ---------- next-edit prediction (models trained for it, e.g. Stage E)
const nextEdit = new NextEditController(s => { const nextEdit = new NextEditController(s => {
api('POST', '/v1/feedback', { event: 'next-edit-accepted', lines: s.newText.split('\n').length }).catch(() => {}) api('POST', '/v1/feedback', { event: 'next-edit-accepted', lines: s.newText.split('\n').length }).catch(() => {})
nextEditAfterAccept('accepted-next-edit')
}) })
let nextEditTimer: NodeJS.Timeout | null = null let nextEditTimer: NodeJS.Timeout | null = null
let nextEditAbort: AbortController | null = null let nextEditAbort: AbortController | null = null
@@ -281,6 +282,15 @@ async function requestNextEdit(editor: vscode.TextEditor, reason: string) {
} }
} }
/** Accepting a suggestion (next-edit or completion) asks for the follow-up right away instead of after the typing pause. */
function nextEditAfterAccept(reason: string) {
const editor = vscode.window.activeTextEditor
if (!editor || !modelSupportsNextEdit() || !settings().get<boolean>('nextEdit.afterAccept', true)) return
if (nextEditTimer) clearTimeout(nextEditTimer)
// Next tick: the accepted text and the cursor move have been applied by then.
nextEditTimer = setTimeout(() => void requestNextEdit(editor, reason), 0)
}
function scheduleNextEdit(editor: vscode.TextEditor, reason: string, delay: number) { function scheduleNextEdit(editor: vscode.TextEditor, reason: string, delay: number) {
if (nextEditTimer) clearTimeout(nextEditTimer) if (nextEditTimer) clearTimeout(nextEditTimer)
nextEditTimer = setTimeout(() => void requestNextEdit(editor, reason), delay) nextEditTimer = setTimeout(() => void requestNextEdit(editor, reason), delay)
@@ -535,6 +545,7 @@ export async function activate(ctx: vscode.ExtensionContext) {
vscode.commands.registerCommand('localAutocomplete.showLog', () => log.show()), vscode.commands.registerCommand('localAutocomplete.showLog', () => log.show()),
vscode.commands.registerCommand('localAutocomplete.accepted', (requestId: string, text: string) => { vscode.commands.registerCommand('localAutocomplete.accepted', (requestId: string, text: string) => {
api('POST', '/v1/feedback', { requestId, event: 'accepted', chars: text.length }).catch(() => {}) api('POST', '/v1/feedback', { requestId, event: 'accepted', chars: text.length }).catch(() => {})
nextEditAfterAccept('accepted-completion')
}), }),
vscode.workspace.onDidChangeConfiguration(e => { vscode.workspace.onDidChangeConfiguration(e => {
if (!e.affectsConfiguration('localAutocomplete')) return if (!e.affectsConfiguration('localAutocomplete')) return