Skip to content

copilot: set up Copilot Chat in VS Code with ug configure --agents vscode - #1316

Draft
xi-db wants to merge 13 commits into
mainfrom
xi-db/stack/vscode-configure
Draft

xi-db wants to merge 13 commits into
mainfrom
xi-db/stack/vscode-configure

Conversation

@xi-db

@xi-db xi-db commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

🥞 Stacked PR

Use this link to review incremental changes.


What

Second of two PRs that bring Databricks models to Copilot Chat in VS Code. The first, #1314, adds the extension. This one makes ug configure --agents vscode set it up, so users never install anything in VS Code by hand.

ug configure --agents vscode:

  1. Configures the workspace the way Cursor's path does: sign-in and workspace state, no model picker. Other agents can be configured in the same --agents list.

  2. Finds VS Code's code command: code, then code-insiders, on PATH or where VS Code's installer puts it. macOS doesn't put code on PATH unless the user runs "Shell Command: Install 'code' command in PATH". The install locations checked:

    OS Path
    macOS /Applications/ or ~/Applications/Visual Studio Code.app/.../bin/code
    Windows %LOCALAPPDATA%\Programs\Microsoft VS Code\bin\code.cmd, or the same under Program Files
    Linux /usr/bin or /snap/bin

    If VS Code isn't found, ug warns and the rest of configure still succeeds.

  3. Writes the extension's settings into the user settings.json for that VS Code: unityGateway.workspace, unityGateway.models and unityGateway.authCommand.

    • Models come from discover_model_services: Claude (newest per family) and GPT, filtered with Copilot CLI's rule (copilot_exclusion_reason), so VS Code offers the same models as ug copilot. Gemini and open models, gpt-oss included, are left out (see Validation).
    • Each model gets the API the extension calls it on: GPT-5 and later use responses, through the existing Copilot rule (now exposed as copilot.uses_responses_api); everything else uses chat_completions.
    • Claude models also get token limits, from the table ug already gives Copilot CLI (copilot_model_limits): 936K in / 32K out for Opus 5.5 and Fable 5.1, 872K / 128K for Sonnet 5.5 and Haiku 5.5. VS Code then shows a 1M context window for them, and Copilot Chat doesn't compact a conversation at 128K. GPT models get none and keep the extension's 128K / 32K.
    • The auth command is the one Claude Code already uses (build_auth_shell_command, or the custom OAuth form).
  4. Installs the extension. ug packs the bundled files from src/ucode/vscode/extension/ into a vsix with the standard library (no Node, no vsce, about 2 ms). It then runs code --install-extension <vsix> --force and deletes the temp file. It reinstalls on every run, even over the same version, because a ug upgrade can change the bundled extension without changing its version. Afterwards it prints what to do next:

    Next, in VS Code:
      1. If VS Code is open, quit it (Cmd+Q) and open it again.
      2. Open Chat, click the model picker, and choose a Databricks model.
    

    On Windows and Linux, step 1 says "close every VS Code window and open it again". A restart is needed only when VS Code was already open, and only after a reinstall. In VS Code 1.143, a first install showed up in an open window within about 3 seconds. After a reinstall, an open window kept serving the build it had loaded until it was reloaded or restarted (checked by installing a build with a different default token limit over the old one). Restarting covers every open window at once.

ug revert removes ug's three settings and uninstalls the extension.

JSONC editing. VS Code's settings.json allows comments and trailing commas, and the repo had no helper for that. config_io gains loads_jsonc and update_jsonc_keys. The editor tokenizes strings, // and /* */ comments, and structure, then changes only the spans of the top-level keys it sets or removes. Everything else stays byte-for-byte, comments included:

  • a new key goes after the last entry's line, so that line's comment stays with it;
  • a removed key takes its own line's comment with it;
  • an unreadable file gets a warning and is left alone;
  • each edit is re-parsed to check the result before it's written.

The unit-test isolation fixture now stubs out VS Code discovery and its settings path, so no test runs the developer's code or touches their settings.

Follow-ups from the #1314 review

Four review comments on #1314, fixed here because #1314 had already merged:

  • A reply has to end with a "finished" event. Both stream readers stopped when the connection closed, so a stream cut off mid-answer reached Copilot Chat as a finished reply. A reply now counts as done only after response.completed (Responses) or a finish_reason of stop or tool_calls (Chat Completions). A stream that ends without one fails with "The response ended before the model finished. Try again."
  • Every early stop is reported. Only max_output_tokens and length used to throw, so a content_filter stop silently truncated the answer. Every other response.incomplete reason and finish_reason now fails with its own message: output token limit, content filter, or "stopped before finishing (<reason>)".
  • Readable names in the model picker. ug configure writes a name for each model with model_picker_label, the helper Codex's picker uses ("Claude Sonnet 5.5", "GPT 6.1 Sol"), and the extension shows it. As in Codex, models whose labels collide keep their full ids. The id still goes to the gateway, and settings without a name show the id.
  • One copy of the icon. The SVG now lives only in src/ucode/vscode/extension/media/, the folder ug ships, and the build no longer copies it. The shipped package.json copy is marked generated, like the bundle.

Fixes from the automated review

From github-actions and Polly (replied in each thread):

  • code failures: a code command that hangs or can't start now warns instead of crashing configure or revert.
  • Revert isolation: a VS Code failure in ug revert shows as failed and no longer stops the other restores.
  • Empty model listing: configure leaves VS Code unchanged and says why, instead of writing an empty model list over a working setup.
  • Revert only runs code when needed: it runs code only when ug's settings are there.
  • --dry-run: VS Code is skipped, because a dry run doesn't save the workspace it would be set up from.
  • Images in tool results: they reach the model. Responses sends them inside function_call_output. Chat Completions sends them in the user message after the results, because the gateway rejects images in a tool message. In VS Code, Claude Haiku 5.5 and GPT 6 Luna each read a PNG through copilot_viewImage and named its colour.
  • Stream errors and tool calls: an error event in a Chat Completions stream fails the reply with its message. Responses tool calls wait for response.completed.
  • Sign-in message: after a second 401 it points to ug configure --agents vscode, not databricks auth login.
  • Shipped package.json: it no longer includes the repo's scripts and devDependencies.

Not in this PR

  • MCP servers for VS Code.
  • Settings in non-default VS Code profiles. The extension is installed for the default profile only.

Validation

  • Unit tests: tests/test_agent_vscode.py (18) and tests/test_config_io.py::TestJsonc (12) cover:
    • the settings merge: the user's comments and keys kept; reruns update values and reinstall the extension; missing files created; unreadable files left alone;
    • revert: only ug's keys removed and the extension uninstalled; nothing changed when VS Code was never configured;
    • finding code: on PATH, Insiders, the macOS app, the Windows user install, none;
    • settings paths per OS, the vsix contents, auth commands, a failed install, and the --agents vscode CLI wiring;
    • the JSONC edge cases: comments, strings containing //, trailing commas, empty files, invalid files.
  • Full unit suite: 5,792 passed. The 11 failures (test_agents_init update prompts, test_e2e_user_agent, test_smart_router toggles) fail the same way on main. just lint passes.
  • By hand, end to end on Linux: an isolated home, a real VS Code 1.143.1, a real workspace.
    • ug configure --agents vscode --profile <profile> found code, wrote 35 models (open models were still included then; the same workspace now gets 23) and the auth command into ~/.config/Code/User/settings.json, and installed databricks.unity-gateway 0.1.0 from the vsix it packed.
    • VS Code was then started from that home with no extension or profile flags. Copilot Chat listed the Databricks models. Agent-mode file tasks completed on claude-haiku-5-5 (Chat Completions) and gpt-6-luna (Responses), with tokens from the configured auth command.
    • ug revert then reported the VS Code extension and settings as removed. The three unityGateway.* keys were gone, the other 7 settings in the file were untouched, and the extension was uninstalled.
  • Longer than one token lifetime: in the same VS Code window, two Agent turns on claude-haiku-5-5 66 minutes apart each appended a line to a file. The first token expired at 18:58 UTC; the second turn, at 19:47, ran the auth command again for a new token, and both lines were written.
  • The configure run used DATABRICKS_BEARER to sign in, because the workspace-only path forces databricks auth login and the test box has no browser (Cursor's path already behaves this way). VS Code itself used the ug auth-token --profile command.
  • Why no open models: I sent each open model the workspace lists two Agent-style turns shaped like the extension's (a tool call, then the tool result). gpt-oss-120b, gpt-oss-20b, deepseek-v4-1-flash, deepseek-v4-1-flash-1017, glm-5-2 and kimi-k2-7-code worked. deepseek-v4-flash-0731, deepseek-v4-pro-0813, glm-5-3, glm-5-3-flash and kimi-k3 failed with 500 Could not route request on both Chat Completions and Responses, with or without tools, and kimi-k3-neo returned 404. gpt-oss-120b also stops without calling a tool when Copilot sends its full tool list. So VS Code offers what Copilot CLI offers.
  • Token limits on Chat Completions: that table was written for the gateway's Anthropic route, so I checked it on the route the extension uses (e2-dogfood). The gateway took an 861K-token prompt to claude-haiku-5-5, 630K-token prompts to Haiku and Sonnet 5.5, and max_tokens of 128K on Haiku and Sonnet 5.5 and 32K on Opus 5.5 and Fable 5.1. Near-1M prompts can still hit a workspace's per-minute input-token rate limit (a 429), which is a separate limit. In VS Code, Manage Language Models lists the four Claude models at 1M and the rest at 160K.
  • Review follow-ups, by hand: after the fixes above, Agent-mode file tasks on Claude Haiku 5.5 (Chat Completions) and GPT 6 Luna (Responses) both completed without an error in real VS Code, so normal streams still count as finished. The picker lists the models by name with the Databricks icon. npm test runs 27 cases, including streams cut off on each route and content_filter stops; tests/test_agent_vscode.py covers the names and the label-collision fallback.
  • Not covered: no tests/integration/ journey. The integration runners have no VS Code, and launching VS Code is outside the integration scope in tests/AGENTS.md. Also not run: Windows or macOS.

This pull request and its description were written by Isaac.

Comment thread src/ucode/agents/vscode.py Outdated
Comment on lines +128 to +131
def _run_code(code: Path, *args: str):
return subprocess_cross_os.run(
[str(code), *args], capture_output=True, text=True, timeout=_CODE_TIMEOUT_S, check=False
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
what happens if code hangs or can't be executed? TimeoutExpired/OSError aren't caught, so ug revert crashes before restore_file/clear_state runs. Configure only catches RuntimeError, so it crashes too. can we catch these and warn instead?

Suggestion:

Suggested change
def _run_code(code: Path, *args: str):
return subprocess_cross_os.run(
[str(code), *args], capture_output=True, text=True, timeout=_CODE_TIMEOUT_S, check=False
)
def _run_code(code: Path, *args: str):
try:
return subprocess_cross_os.run(
[str(code), *args], capture_output=True, text=True, timeout=_CODE_TIMEOUT_S, check=False
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise RuntimeError(f"`{code} {' '.join(args)}` failed: {exc}") from exc

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 918f4fa. _run_code now turns TimeoutExpired and OSError into the same RuntimeError as any other code failure, so configure warns instead of crashing. ug revert also wraps the VS Code step, so a failure there shows as failed and the other restores still run.

Comment thread src/ucode/agents/vscode.py Outdated
Comment on lines +205 to +206
if not models:
print_warning(f"No models for VS Code: {reason}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · low
if discovery fails for a transient reason, we still write unityGateway.models: [], which wipes a working list from an earlier configure. can we skip writing models (or the whole update) when the list is empty?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 918f4fa. When the listing comes back empty, configure warns with the reason and leaves VS Code unchanged (settings and extension). A working setup from an earlier run stays as it was, and a workspace switch can't pair the old models with the new workspace.

Comment thread src/ucode/agents/vscode.py Outdated
workspace = state["workspace"]
models, reason = model_settings(workspace, launch_token(state, workspace))
if not models:
print_warning(f"No models for VS Code: {reason}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · low
if the workspace only has gemini models, reason is None and this prints No models for VS Code: None. can we fall back to a message that says why, e.g. no supported models?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 918f4fa. Without a reason it now says No Claude or GPT models for VS Code (the workspace has none). VS Code was left unchanged.

Comment thread src/ucode/cli.py Outdated
Comment on lines +1547 to +1549
from ucode.agents import vscode

vscode_result = vscode.revert()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
this now runs code --list-extensions (120s timeout) on every revert, including for users who never set up vscode. can we skip it unless ug recorded a vscode configure, and keep a failure here from blocking the rest of revert?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 918f4fa. Revert now runs code only when it found ug's unityGateway.* settings, since configure writes them before it installs. A failure in this step no longer stops the rest of revert either.

Comment thread src/ucode/cli.py
if wants_vscode:
from ucode.agents import vscode

vscode.configure(load_state())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
what happens under --dry-run? save_state is skipped then, so load_state() gives {} on a fresh machine and state["workspace"] raises KeyError. If a workspace was saved before, this configures that stale one instead of the requested one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 918f4fa. --dry-run now skips VS Code with a note: a dry run doesn't save the workspace that VS Code would be set up from.

@@ -0,0 +1,2 @@
"use strict";var U=Object.create;var f=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var K=(e,n)=>{for(var t in n)f(e,t,{get:n[t],enumerable:!0})},x=(e,n,t,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of B(n))!H.call(e,a)&&a!==t&&f(e,a,{get:()=>n[a],enumerable:!(s=F(n,a))||s.enumerable});return e};var C=(e,n,t)=>(t=e!=null?U(W(e)):{},x(n||!e||!e.__esModule?f(t,"default",{value:e,enumerable:!0}):t,e)),Y=e=>x(f({},"__esModule",{value:!0}),e);var ne={};K(ne,{DatabricksModels:()=>v,activate:()=>te,readSettings:()=>N,userAgent:()=>q});module.exports=Y(ne);var c=C(require("vscode"));var b="0.1.0";var T=require("node:child_process"),X=5*6e4,Q=60*6e4,V=e=>new Promise((n,t)=>{(0,T.exec)(e,{timeout:6e4,windowsHide:!0},(s,a,o)=>{s?t(new Error(`Couldn't get a Databricks token: \`${e}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(a).trim())})}),Z=e=>{try{let{exp:n}=JSON.parse(Buffer.from(e.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+Q},h=class{constructor(n,t=()=>{}){this.command=n;this.onFetch=t}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-X>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=V(n).then(t=>{if(!t)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:t,expiresAt:Z(t)},this.onFetch(this.cached.expiresAt),t}).finally(()=>{this.pending=void 0}),this.pending}};var i=C(require("vscode")),_=3,ee=1e3,y=e=>e instanceof i.LanguageModelTextPart?e.value:e instanceof i.LanguageModelDataPart&&!e.mimeType.startsWith("image/")?new TextDecoder().decode(e.data):e instanceof i.LanguageModelPromptTsxPart?JSON.stringify(e.value):"",S=e=>e instanceof i.LanguageModelDataPart&&e.mimeType.startsWith("image/")?`data:${e.mimeType};base64,${Buffer.from(e.data).toString("base64")}`:void 0,R=e=>e.inputSchema??{type:"object",properties:{}},L=(e,n,t)=>{let s;try{s=t.trim()?JSON.parse(t):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${t}`);return new i.LanguageModelToolCallPart(e,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(e,n)=>e.tools.length?{tools:e.tools.map(n),...e.toolRequired?{tool_choice:"required"}:{}}:{},E=e=>{if(typeof e=="string")return Math.ceil(e.length/4);let n=0,t=0;for(let s of e.content){let a=s instanceof i.LanguageModelToolResultPart?s.content:[s];for(let o of a)n+=o instanceof i.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,t+=S(o)===void 0?0:1}return Math.ceil(n/4)+t*ee},j=e=>({results:e.filter(n=>n instanceof i.LanguageModelToolResultPart),calls:e.filter(n=>n instanceof i.LanguageModelToolCallPart),text:e.map(y).join(""),images:e.map(S).filter(n=>n!==void 0)});function D(e){let n=e.messages.flatMap(t=>{let{results:s,calls:a,text:o,images:u}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant){let r=a.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||r.length?[{role:"assistant",content:o||null,...r.length?{tool_calls:r}:{}}]:[]}let l=[...o?[{type:"text",text:o}]:[],...u.map(r=>({type:"image_url",image_url:{url:r}}))];return[...s.map(r=>({role:"tool",tool_call_id:r.callId,content:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:e.model,messages:n,stream:!0,...A(e,t=>({type:"function",function:{name:t.name,description:t.description,parameters:R(t)}}))}}async function*O(e){let n=[];for await(let t of e)for(let s of t.choices??[]){s.delta?.content&&(yield new i.LanguageModelTextPart(s.delta.content));for(let a of s.delta?.tool_calls??[]){let o=n[a.index??0]??={id:"",name:"",args:""};o.id=a.id||o.id,o.name=a.function?.name||o.name,o.args+=a.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let t of n.filter(Boolean))yield L(t.id,t.name,t.args)}function I(e){let n=e.messages.flatMap(t=>{let{results:s,calls:a,text:o,images:u}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...a.map(r=>({type:"function_call",call_id:r.callId,name:r.name,arguments:JSON.stringify(r.input)}))];let l=[...o?[{type:"input_text",text:o}]:[],...u.map(r=>({type:"input_image",image_url:r,detail:"auto"}))];return[...s.map(r=>({type:"function_call_output",call_id:r.callId,output:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:e.model,input:n,stream:!0,store:!1,...A(e,t=>({type:"function",name:t.name,description:t.description,parameters:R(t),strict:!1}))}}async function*G(e){for await(let n of e){let t=n;if(t.type==="response.output_text.delta"&&t.delta)yield new i.LanguageModelTextPart(t.delta);else if(t.type==="response.output_item.done"&&t.item?.type==="function_call")yield L(t.item.call_id??"",t.item.name??"",t.item.arguments??"");else{if(t.type==="response.incomplete"&&t.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(t.type==="response.failed"||t.type==="error")throw new Error(t.response?.error?.message??t.message??"The model response failed.")}}}async function*k(e,n,t,s,a,o){let u=m=>fetch(`${e}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...a,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:o}),l=await s.get(),r=await u(l);if(r.status===401&&(await r.body?.cancel(),l=await s.renew(l),r=await u(l)),r.status===401)throw await r.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${e}\`, then retry.`);if(!r.ok||!r.body)throw new Error(`AI Gateway returned ${r.status}: ${(await r.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of r.body){p+=g.decode(m,{stream:!0});let M=p.split(`
`);p=M.pop()??"";for(let J of M){let w=$(J);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let d=$(p+g.decode());d&&d!=="[DONE]"&&(yield JSON.parse(d))}var $=e=>e.startsWith("data:")?e.slice(5).trim():"";var N=e=>{let n=e("models");return{workspace:String(e("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(t=>typeof t?.id=="string"&&t.id!==""&&(t.api==="chat_completions"||t.api==="responses")),authCommand:String(e("authCommand")??"")}},q=e=>`ucode/${b} githubcopilotchat${typeof e=="string"?`/${e}`:""}`,v=class{constructor(n,t,s=()=>({})){this.settings=n;this.log=t;this.headers=s}settings;log;headers;changed=new c.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:t,authCommand:s}=this.settings();return!n||!s||t.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):t.map(({id:a,api:o})=>({id:a,api:o,name:a,family:a.split(".").pop()??a,version:b,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||a.includes("claude")}}))}async provideLanguageModelChatResponse(n,t,s,a,o){let{workspace:u,authCommand:l}=this.settings();this.tokens??=new h(l,d=>this.log(`Fetched a Databricks token (expires ${new Date(d).toISOString()})`));let r={model:n.id,messages:t,tools:s.tools??[],toolRequired:s.toolMode===c.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let d=n.api==="responses"?G(k(u,"responses",I(r),this.tokens,this.headers(),g.signal)):O(k(u,"chat/completions",D(r),this.tokens,this.headers(),g.signal));for await(let m of d)a.report(m)}catch(d){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${d instanceof Error?d.message:String(d)}`),d}finally{p.dispose()}}async provideTokenCount(n,t){return E(t)}};function te(e){let n=c.window.createOutputChannel("Unity Gateway",{log:!0}),t=()=>{let o=c.workspace.getConfiguration("unityGateway");return N(u=>o.get(u))},s=()=>({"User-Agent":q(c.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version)}),a=new v(t,o=>n.info(o),s);e.subscriptions.push(n,c.lm.registerLanguageModelChatProvider("databricks-unity-gateway",a),c.commands.registerCommand("unityGateway.refreshModels",()=>a.refresh()),c.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&a.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings,userAgent});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
what happens if the gateway sends an {"error":...} SSE event on chat/completions? O() only reads choices, so the user gets an empty reply with no error. can we throw on error like the responses path does?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1d76882. An error event now fails the reply with the gateway's message. Since 7867ee4, a stream that ends without a finish_reason also fails as cut off.

"directory": "vscode-extension"
},
"engines": {
"vscode": "^1.128.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other · low
is ^1.128.0 actually needed? types target 1.125. users on older VS Code will get an install failure from ug configure. can we lower it to the oldest version that has the APIs this uses?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping ^1.128.0. 1.128 is the first VS Code that offers extension models in the Copilot harness as well as the Local one, and this extension relies on that. The types target 1.125 only because @types/vscode 1.128 wasn't published. On an older VS Code, the install fails with code's message as a warning, and the rest of configure still succeeds.

Comment thread src/ucode/vscode/extension/package.json Outdated
]
},
"scripts": {
"build": "esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e \"require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')\"",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
this copy ships scripts/devDependencies with ../src/... paths that don't work from here, and they end up in the vsix. can we strip dev-only fields when copying?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1d76882. The build now writes the shipped package.json without scripts and devDependencies.

Comment thread tests/test_agent_vscode.py Outdated
Comment on lines +204 to +207
def test_finds_the_macos_app_when_code_isnt_on_path(self, monkeypatch, tmp_path):
monkeypatch.setattr(vscode.shutil, "which", lambda name: None)
monkeypatch.setattr(vscode, "current_os", lambda: OS.MACOS)
monkeypatch.setattr(vscode.Path, "home", lambda: tmp_path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test · medium
_install_locations checks /Applications before ~/Applications. On a mac with VS Code in /Applications this test returns that real path and fails. can we stub _install_locations or the roots?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 918f4fa. The lookup is now tested in two parts, and neither reads the real /Applications: one checks the locations it looks in, in order, and the other checks the fallback to the first one that exists, using stubbed locations.

Comment on lines +271 to +274
monkeypatch.setattr(
"ucode.cli._configure_shared_workspace_states",
lambda *args, **kwargs: shared.append(kwargs) or [],
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test · low
this patches 4 functions, one of them a private cli helper, and only checks call shapes. is it worth keeping, or can it check observable output (settings written) instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping it, and it now covers --dry-run too (918f4fa). Checking the outcome of the real path would need a signed-in workspace and an installed VS Code. Writing the settings and installing the extension are covered in TestConfigure. This test pins the CLI routing: --agents vscode sets up the workspace without the model-agent path, then hands that state to VS Code.

@github-actions

Copy link
Copy Markdown
Contributor

Polly AI Review

Blocking issues

  1. [P1] VS Code failures interrupt unrelated agent restoration — src/ucode/cli.py:1549
    can we isolate VS Code cleanup failures? Settings read/write errors or a code timeout stop the following Claude/Codex/Pi restores, even without prior VS Code setup.

    Regression: extend tests/test_agent_vscode.py::TestRevert and the cleanup-failure coverage in tests/test_cli.py to verify other restores continue while failed cleanup remains reported and recoverable.

  2. [P2] Failed discovery erases a working model catalog — src/ucode/agents/vscode.py:205
    can we leave the previous configuration intact when discovery fails? A network failure returns an empty catalog plus a reason, but configure writes unityGateway.models: [].

    Regression: extend tests/test_agent_vscode.py::TestConfigure::test_reruns_update_values_without_reinstalling_the_same_version with discovery failure. Cover workspace switching without rebinding the old catalog to the new workspace.

  3. [P2] Image-returning tools lose their results — vscode-extension/src/formats.ts:100, vscode-extension/src/formats.ts:160
    can we preserve images inside tool results? Both serializers flatten results through textOf, so screenshot-only results become empty strings and mixed results lose their images.

    Regression: extend sends the conversation, images and tools and sends the whole conversation statelessly in vscode-extension/test/formats.test.ts with nested tool-result images.

  4. [P2] Chat streaming errors become successful empty turns — vscode-extension/src/formats.ts:122
    can we reject in-band error envelopes? An HTTP-200 stream containing {"error":{"message":"…"}} followed by [DONE] currently resolves successfully without reporting the failure.

    Regression: extend vscode-extension/test/formats.test.ts beside fails on a length stop and on arguments that aren't an object with an error-envelope case.

  5. [P2] Incomplete streams can dispatch unfinished tool calls — vscode-extension/src/formats.ts:137
    can we require successful completion before releasing buffered tool calls? A clean early EOF without a finish reason or [DONE] still emits a dispatchable call.

    Regression: extend streams text and assembles a tool call from its fragments in vscode-extension/test/formats.test.ts with premature EOF and assert rejection without a tool call.

Security vulnerabilities

No verified security vulnerability found. The command and destination settings are application-scoped, preventing ordinary repository workspace settings from overriding them. Live security and dependency-advisory checks were not performed.

Non-blocking notes

  • vscode-extension/test/extension.test.ts:126 — can sends each model's turns to its API with the User-Agent record and assert that header? Its fetch stub currently records authorization, URL and body, but not User-Agent.

Approach

The module boundaries and JSONC-preserving editor fit the existing patterns. No existing JSONC editor was found to reuse. For cleanup, follow the existing Desktop failure-result pattern so optional integration failures do not interrupt unrelated restoration.

Summary

Diff size (computed): 27 files; +2489 / -7 lines (2496 changed text lines); 0 binary files.

Category Files Added Deleted Share of changed text lines
Tests and test support 8 779 0 31.2%
Documentation 1 22 0 0.9%
Dependencies and lockfiles 3 731 0 29.3%
Implementation / other 15 957 7 38.6%

Shares use additions + deletions, including generated text and lockfiles. Binary files have no line count. Tests include fixtures/helpers under test paths and colocated test/spec files; categories are path-based, not coverage measurements.

Changes

The PR connects workspace configuration to a bundled VS Code Copilot provider. The full snapshot includes the prerequisite extension, Python installation and cleanup, JSONC editing, documentation, test support, build checks, dependency metadata, a lockfile and generated JavaScript.

The size is largely explained by delivering both the extension and its configure integration in this stacked snapshot. No concrete redundant implementation or scaffolding was found that could be removed while preserving the intended behavior.

Tests

Test-by-test assessment

Assessment covers all eight changed test/support files. Existing CLI cleanup-failure coverage provides a suitable place to extend restoration checks. The demonstrated serializer and stream failures belong in the existing unit/component tests, not additional E2Es.

Three extension failures were reproduced through isolated synthetic invocations of the bundled extension, with network and process execution stubbed and credentials unavailable. Repository test suites, builds and live VS Code flows were not run; Python findings are based on control-flow inspection.

Test / case Behavior protected Layer Needed? Action / rationale
tests/conftest.py::_isolate_ucode_state — VS Code discovery and settings-path stubs Prevents real executable invocation and user-settings writes Fixture keep Required isolation for existing tests.
tests/test_agent_vscode.py — REAL_FIND_CODE, REAL_SETTINGS_PATH, WS, STATE, USER_SETTINGS Explicit shared inputs and access to real path helpers Support keep Small setup supporting the relevant boundaries.
tests/test_agent_vscode.py::FakeCode, fixture code, _printed, _settings, _settings_values Controlled installation, discovery and settings observations Support keep Reusable fake with assertions remaining in tests.
tests/test_agent_vscode.py::TestConfigure::test_merges_settings_keeping_the_users_own User comments/keys, model API selection, deduplication and filtering Unit keep Verifies the configure contract beyond the JSONC helper itself.
tests/test_agent_vscode.py::TestConfigure::test_creates_missing_settings_and_installs_the_packed_extension New settings file, VSIX contents and forced installation Component keep Distinct packaging and installation contract.
tests/test_agent_vscode.py::TestConfigure::test_reruns_update_values_without_reinstalling_the_same_version Updated settings with version-based installation idempotency Unit keep Extend with failed discovery and workspace transitions as requested above.
tests/test_agent_vscode.py::TestConfigure::test_leaves_unreadable_settings_alone Malformed JSONC remains untouched Unit keep Useful parse-failure boundary; does not cover filesystem permission failures.
tests/test_agent_vscode.py::TestConfigure::test_warns_when_vscode_isnt_found Missing executable produces a warning without settings writes Unit keep Protects the optional-integration early return.
tests/test_agent_vscode.py::TestConfigure::test_reports_a_failed_install Nonzero installation exit becomes a reported failure Unit keep Distinct from timeout and spawn exceptions.
tests/test_agent_vscode.py::TestRevert::test_removes_only_ugs_settings_and_uninstalls Owned-key removal, unrelated settings preservation and uninstall Unit keep Core cleanup ownership contract.
tests/test_agent_vscode.py::TestRevert::test_does_nothing_when_vscode_was_never_configured Unconfigured settings remain unchanged Unit keep Extend with listing timeout/spawn failure; current coverage misses global cleanup interruption.
tests/test_agent_vscode.py::TestFindCode::test_prefers_code_on_path Stable executable preference Unit keep Distinct discovery branch.
tests/test_agent_vscode.py::TestFindCode::test_falls_back_to_insiders Insiders fallback and product identity Unit keep Guards selection of the matching settings location.
tests/test_agent_vscode.py::TestFindCode::test_finds_the_macos_app_when_code_isnt_on_path macOS application-bundle discovery Unit keep Platform-specific fallback.
tests/test_agent_vscode.py::TestFindCode::test_finds_the_windows_user_install Windows user-install discovery Unit keep Platform-specific fallback.
tests/test_agent_vscode.py::TestFindCode::test_none_when_vscode_isnt_installed Real discovery returns no executable Unit keep Other tests generally replace discovery with a stub.
tests/test_agent_vscode.py::TestSettingsPath::test_user_settings_per_os[LINUX], [WINDOWS] Platform-specific user-settings roots Unit keep Appropriate low-layer path matrix.
tests/test_agent_vscode.py::TestSettingsPath::test_macos macOS settings location and Insiders folder Unit keep Uses distinct home-directory setup.
tests/test_agent_vscode.py::TestAuthCommand::test_pat_and_custom_oauth Selection of existing PAT/custom-OAuth command builders Unit keep Protects authentication branch selection without real credentials.
tests/test_agent_vscode.py::test_configure_agents_vscode_sets_up_the_workspace_then_vscode Workspace-only setup followed by VS Code configuration CLI integration keep Necessary orchestration coverage.
tests/test_config_io.py::TestJsonc.SETTINGS Shared commented settings input Support keep Representative input reused across editor assertions.
tests/test_config_io.py::TestJsonc::test_loads_comments_and_trailing_commas JSONC parsing and comment markers inside strings Unit keep Direct parser contract.
tests/test_config_io.py::TestJsonc::test_sets_keys_and_keeps_everything_else Replacement/insertion while preserving unrelated bytes Unit keep Core preservation behavior.
tests/test_config_io.py::TestJsonc::test_removes_keys_with_their_line_comment Owned-entry/comment removal and absent-key handling Unit keep Direct dependency of safe revert.
tests/test_config_io.py::TestJsonc::test_removing_the_last_key_drops_the_comma_before_it Valid syntax after last/only-entry removal Unit keep Distinct structural boundary.
tests/test_config_io.py::TestJsonc::test_fills_an_empty_file — empty text, comment-only text, {} Initialization of empty settings inputs Unit keep Parameterized inputs exercise the supported initialization forms.
tests/test_config_io.py::TestJsonc::test_rejects_what_isnt_one_object — unterminated value, array, adjacent objects, missing colon, unterminated string Invalid input rejection before editing Unit keep Focused negative-input matrix at the lowest reliable layer.
vscode-extension/test/auth.test.ts — reuses a token until it nears expiry Token reuse and refresh near expiry Component keep Protects cache lifetime behavior.
vscode-extension/test/auth.test.ts — runs the command once for concurrent requests Concurrent token-fetch deduplication Component keep Distinct from sequential cache reuse.
vscode-extension/test/auth.test.ts — forces a new token after a rejection, but not after a stale one Renewal after rejection without redundant stale-token refresh Component keep Important authentication recovery transition.
vscode-extension/test/auth.test.ts — waits for a fetch in flight before deciding to force one Rejection handling during pending token acquisition Component keep Covers a distinct concurrency race.
vscode-extension/test/auth.test.ts — reports the command's failure and empty output Failed command and empty successful output Component keep Both materially different authentication failures are meaningful.
vscode-extension/test/auth.test.ts — reads exp from a JWT and gives other tokens an hour JWT expiry and opaque-token fallback Unit keep Appropriate isolated input coverage.
vscode-extension/test/extension.test.ts — yields each event's JSON until [DONE] SSE chunk assembly and terminal marker handling Component keep Transport decoding is distinct from adapter error semantics.
vscode-extension/test/extension.test.ts — keeps a last event that has no trailing newline Final buffered event decoding Component keep A final data line alone does not establish successful model completion.
vscode-extension/test/extension.test.ts — retries once with a new token after a 401, then asks the user to sign in Token renewal, bounded retry and repeated rejection Component keep Useful transport/authentication contract without live inference.
vscode-extension/test/extension.test.ts — reports other gateway errors with their body Non-success HTTP status diagnostics Component keep Does not replace the missing HTTP-200 stream-error regression.
vscode-extension/test/extension.test.ts — are read from user settings only Application-scoped configuration declarations Manifest contract keep Static protection of the workspace trust boundary; not real-host validation.
vscode-extension/test/extension.test.ts — names ug's extension and Copilot Chat's version User-Agent construction with/without Copilot metadata Unit keep Distinct from proving that fetch receives the header.
vscode-extension/test/extension.test.ts — keeps well-formed models and trims the workspace URL Model validation and URL normalization Unit keep Input matrix belongs at the parser layer.
vscode-extension/test/extension.test.ts — lists the configured models, or none with a hint when settings are missing Provider metadata and missing-configuration hint Component keep Tests externally visible provider behavior.
vscode-extension/test/extension.test.ts — sends each model's turns to its API with the User-Agent Endpoint selection and response forwarding Component keep Record/assert User-Agent as noted above; the current assertion does not protect it.
vscode-extension/test/formats.test.ts — sends the conversation, images and tools Chat roles, direct images, calls/results and tool schemas Unit keep Extend with nested result images; direct images do not cover the demonstrated loss.
vscode-extension/test/formats.test.ts — gives a tool without an input schema an empty one, and omits tools when there are none Schema fallback and optional tool fields Unit keep Related option boundaries at the correct layer.
vscode-extension/test/formats.test.ts — streams text and assembles a tool call from its fragments Text forwarding and fragmented tool arguments Unit keep Extend with early EOF and rejection before tool dispatch.
vscode-extension/test/formats.test.ts — fails on a length stop and on arguments that aren't an object Length-stop and malformed-argument failures Unit keep Meaningful negative cases; add the distinct stream-error-envelope regression nearby.
vscode-extension/test/formats.test.ts — counts text and gives each image a flat cost instead of its bytes Text/image estimates, including nested images Unit keep Protects estimation, not image serialization.
vscode-extension/test/formats.test.ts — sends the whole conversation statelessly Responses history, tools, direct images and store:false Unit keep Extend with nested result images for the second wire format.
vscode-extension/test/formats.test.ts — streams text and each finished function call Responses text and completed function-call events Unit keep Distinct protocol behavior; not redundant with Chat coverage.
vscode-extension/test/formats.test.ts — fails on a token-limit stop and on a failed response Responses incompletion and upstream failure Unit keep Valuable failure assertions at the smallest reliable layer.
vscode-extension/test/extension.test.ts — sse, stubFetch, realFetch/afterEach, settings Explicit transport fixtures, request capture and stub cleanup Support keep Small reusable setup; extend header capture for the existing User-Agent assertion.
vscode-extension/test/formats.test.ts — conversation, turn Equivalent serializer inputs across both APIs Support keep Shared inputs expose protocol differences without repeated setup.
vscode-extension/test/support.ts — SYSTEM, USER, ASSISTANT, message, text, image, call, result, weatherTool Caller-controlled role, message-part and tool inputs Support keep Lightweight primitives leave faults and assertions in individual tests.
vscode-extension/test/support.ts — jwt, tokenCommand Synthetic expiry and process-based token acquisition Process support keep The real process boundary is relevant to the auth-command contract.
vscode-extension/test/support.ts — events, collect, rejectsWith Explicit stream input, result collection and error assertions Support keep Narrow async helpers without hidden scenarios.
vscode-extension/test/vscode.ts — message-part classes, LanguageModelPromptTsxPart, role/tool-mode constants Construction and type checks outside VS Code API shim keep Necessary unit-test stand-ins; do not establish real-host compatibility.
vscode-extension/test/vscode.ts — EventEmitter, event/fire/disposal stubs Provider construction and event plumbing API shim keep Minimal host substitute, not evidence of actual VS Code lifecycle behavior.

Real installation, activation and Copilot model selection remain unverified host boundaries. To check them manually, run ug configure --agents vscode in a disposable user profile, open Copilot Chat, select a configured model and exercise a text turn and an image-returning tool. Then run ug revert and verify unrelated settings remain.

Scope

No clearly unrelated changes were found. The prerequisite extension, generated bundle, dependencies, configure integration, tests and documentation support the stated Copilot Chat setup outcome. The truncated description limits assessment of its reported validation, but does not establish a scope violation.


Automated review by Polly · workflow run

@xi-db
xi-db force-pushed the xi-db/stack/vscode-configure branch 4 times, most recently from 5dfb9b9 to b756487 Compare October 28, 2026 14:22
yaoharry pushed a commit to yaoharry/unity-gateway that referenced this pull request Oct 28, 2026
…ilot Chat (databricks#1314)

## 🥞 Stacked PR
Use this
[link](https://github.com/databricks/unity-gateway/pull/1314/files) to
review incremental changes.
-
[**stack/vscode-extension**](databricks#1314)
[[Files
changed](https://github.com/databricks/unity-gateway/pull/1314/files)] ←
_this PR_
-
[stack/vscode-configure](databricks#1316)
[[Files
changed](https://github.com/databricks/unity-gateway/pull/1316/files/238af81c0cb95b41cbafb910d00da82005f181c5..3904468a7bfada1c978a78bf69ffe2e3d29d98ce)]

---------
## What

First of two PRs that bring Databricks models to Copilot Chat in VS
Code. This PR adds the extension; the next one makes `ug configure
--agents vscode` pack, install and configure it.

The extension (`vscode-extension/`, TypeScript, no runtime dependencies)
registers a language model chat provider. It puts the workspace's AI
Gateway models in the chat model picker, in both Ask and Agent mode and
in both the Local and Copilot harnesses. It calls the gateway directly
with `fetch`, with no local proxy:

- **Chat Completions**
(`{workspace}/ai-gateway/mlflow/v1/chat/completions`) for Claude. The
next PR's `ug configure` offers only Claude and GPT models, the same set
as Copilot CLI.
- **Responses** (`{workspace}/ai-gateway/mlflow/v1/responses`) for GPT-5
and later. It's stateless: `store: false`, and the whole conversation is
sent each turn. GPT-5.6 and later reject function tools on Chat
Completions, as do the codex and pro models, so they need this route for
Agent mode.
- Streaming, tool calls, tool results and images work on both.
- While a Claude model thinks, the gateway sends the content of a Chat
Completions chunk as blocks rather than a string. The extension takes
the text out of them and drops the reasoning: handing a block to Copilot
Chat fails the turn.

Settings, which `ug configure` will write to the user settings:

| Setting | Value |
|---|---|
| `unityGateway.workspace` | Workspace URL |
| `unityGateway.models` | `[{"id": "<model service>", "api":
"chat_completions" \| "responses", "maxInputTokens"?: n,
"maxOutputTokens"?: n}]` |
| `unityGateway.authCommand` | The `ug auth-token --host ...` command ug
already writes for other agents |

All three are `"scope": "application"`, so VS Code reads them from user
settings only. A repository's `.vscode/settings.json` can't redirect the
token to another URL, or pick the command that prints it.

**Auth.** The extension runs the auth command when it needs a token,
keeps the token in memory until five minutes before its `exp`, and
shares one fetch between concurrent requests. When the gateway answers
401 it runs the command again with `--force-refresh`, because the
Databricks CLI can return its cached token while `exp` is still ahead,
and retries once. If that also fails, it reports "Databricks rejected
the sign-in. Run `databricks auth login --host ...`, then retry." It
stores no credentials, and the token is never logged.

**Other behavior:**
- **User-Agent:** `ucode/<extension version> githubcopilotchat/<Copilot
Chat version>`. The gateway already attributes the `githubcopilotchat`
token to Copilot Chat.
- **Model info:** the model `family` is the model's own name
(`claude-sonnet-5-5`, `gpt-6-luna`), which Copilot Chat uses to pick its
prompts and tools. The gateway doesn't report context limits, so a
model's limits come from its `maxInputTokens` / `maxOutputTokens`
settings, and a model without them gets 128K input and 32K output
(Copilot's own default). Image input is on for Claude and Responses
models. Token counts are about four characters per token, plus a flat
1,000 per image.
- **Icon:** the provider shows the Databricks logo
(`media/databricks.svg`, in the brand red) in the model picker.
- **`Unity Gateway: Refresh Models`** re-reads the settings; settings
changes also refresh.
- **Logging:** the **Unity Gateway** output channel logs token fetches
(expiry time only) and failed requests.
- **`extensionKind: ["ui"]`**, engine `^1.128.0` (the first version that
offers extension models in the Copilot harness).

**Packaging.**
- `npm run build` (esbuild, minified) writes
`src/ucode/vscode/extension/out/extension.js` (9 KB) and copies
`package.json` and `media/` next to it. Both are committed, so the wheel
ships them and ug can pack the vsix with the standard library: no Node
on users' machines, no binary in the repo.
- `just vscode-build` rebuilds them, and rebuilds are byte-identical.
- `.gitattributes` marks the bundle as generated.

**CI.** A new `VS Code extension` job runs `npm ci` (with the npm
cache), typecheck and the tests. It then rebuilds and fails if the
committed files under `src/ucode/vscode/extension/` changed. The `test`
check that branch rules require now needs this job too.

`package-lock.json` records the public npm registry; `.npmrc` sets
`replace-registry-host=always`, so `npm ci` fetches the same tarballs
from whatever registry is configured locally.

## Not in this PR

- `ug configure` doesn't install or configure the extension yet; that's
the next PR.
- No MCP registration for VS Code.
- Claude goes through Chat Completions, so Anthropic-only features,
prompt caching among them, aren't used.

## Validation

- `npm run typecheck` is clean. `npm test` runs 25 cases with Node's own
test runner (esbuild compiles them, with `vscode` aliased to a test
stand-in, so the only dev dependencies are esbuild, TypeScript and type
packages):
- **auth**, against a real shell command: reuse until near expiry, one
run for concurrent requests, `--force-refresh` after a rejection but not
after a stale one or while a fetch is in flight, command failure and
empty output;
- **both request formats**: message, tool, tool-result and image
conversion; tools without an input schema; fragment assembly; a thinking
model's content blocks; token-limit stops; arguments that aren't an
object; failed responses; token estimates that don't count image bytes;
- **the gateway client**: SSE across chunk boundaries, `[DONE]` and a
last event without a trailing newline; one forced retry after 401, then
the sign-in message; other errors with the gateway's body;
- **the provider**: model list with configured or default token limits,
settings with malformed limits, missing settings, routing by `api` with
the User-Agent;
- **the manifest and User-Agent**: every setting is application-scoped;
the provider's icon file exists; the User-Agent names the extension and
Copilot Chat versions.
- `just lint` passes. The PR changes no Python.
- **By hand** with real VS Code 1.143.1 (Copilot Chat 0.71.0), signed
out, under xvfb on Linux, against a real workspace. The extension was
installed from a vsix packed from the committed files, and the settings
were written to user settings by hand. Each case asked Agent mode to
create a file with a given value:
- **Claude** (`claude-haiku-5-5`, Chat Completions): file written, in
both the Local and the Copilot harness.
- **GPT** (`gpt-6-luna`, Responses): file written, in both the Local and
the Copilot harness.
  - **`deepseek-v4-1-flash`** (Chat Completions): file written.
- **Workspace override:** a workspace `.vscode/settings.json` set
`unityGateway.workspace` to another host and `unityGateway.authCommand`
to a different command. Both were ignored: the task completed through
the user-level settings, and the workspace's command never ran.
- **`gpt-oss-120b`**, tested on an earlier version: with Copilot's full
49-tool request, the model reasons about the tool call and then stops
without making it. The gateway's response contains no tool call.
- Not run: Windows or macOS, or a session longer than one token
lifetime. The over-an-hour check comes with the configure PR.

This pull request and its description were written by Isaac.

---------

Co-authored-by: Isaac <no-reply@databricks.com>
xi-db and others added 8 commits October 29, 2026 10:25
`ug configure --agents vscode` configures the workspace the way Cursor's
path does (no model picker), then:

- finds VS Code's `code` command (or `code-insiders`), on PATH or where
  the installer puts it, since macOS doesn't add it to PATH by default;
- packs the extension bundled with ug into a vsix with the standard
  library and installs it, unless that version is already installed;
- writes unityGateway.workspace, unityGateway.models and
  unityGateway.authCommand into the user settings.json. Models come from
  discover_model_services: Claude, GPT and open models (not Gemini), GPT-5
  and later on the Responses API, the rest on Chat Completions. The auth
  command is the one Claude Code already uses.

The settings are edited with a new JSONC helper in config_io that changes
only ug's top-level keys and keeps the user's comments and formatting.
`ug revert` removes those keys and uninstalls the extension.

Co-authored-by: Isaac <no-reply@databricks.com>
The extension now ships its provider icon next to the bundle and its
manifest. Pack every file under the bundled extension folder instead of
naming the two, so a new asset doesn't need a change here.

Co-authored-by: Isaac <no-reply@databricks.com>
Configure skipped the install when VS Code already had the same extension
version. A ug upgrade can change the bundled extension without changing its
version, and users then kept the old build. Install with --force every time.

Co-authored-by: Isaac <no-reply@databricks.com>
Each Claude entry in unityGateway.models now carries maxInputTokens and
maxOutputTokens from the limits ug already gives Copilot CLI
(copilot_model_limits), e.g. 936K / 32K for Opus 5.5 and 872K / 128K for
Sonnet 5.5. Those limits hold on the Chat Completions route the extension
uses: the gateway took an 861K-token prompt to claude-haiku-5-5 and a
128K max_tokens on Sonnet 5.5. GPT and open models get no entry, so they
keep the extension's 128K / 32K.

Co-authored-by: Isaac <no-reply@databricks.com>
VS Code applies the settings right away, but a window that's already open
keeps running the extension build it loaded, and configure now reinstalls
on every run. Print a note to reload open windows.

Co-authored-by: Isaac <no-reply@databricks.com>
"Reload open VS Code windows (Developer: Reload Window)" assumed people know
the Command Palette. Print the steps instead, with this OS's shortcuts:

    Next, in VS Code:
      1. If VS Code is already open, reload each window:
         press Cmd+Shift+P, type "Reload Window", and press Enter.
         Or quit VS Code (Cmd+Q) and open it again.
      2. Open Chat, click the model picker, and choose a Databricks model.

The reload matters only for a window that's already open: in VS Code
1.143, a first install shows up in an open window within seconds, but a
reinstall doesn't, and the window keeps running the build it loaded.

Co-authored-by: Isaac <no-reply@databricks.com>
Reloading each window through the Command Palette was more to explain, and
it misses any window the user doesn't reload. Restarting VS Code loads the
new extension build in every window at once:

    Next, in VS Code:
      1. If VS Code is open, quit it (Cmd+Q) and open it again.
      2. Open Chat, click the model picker, and choose a Databricks model.

Windows and Linux say "close every VS Code window and open it again".

Co-authored-by: Isaac <no-reply@databricks.com>
Configure wrote every open model the workspace lists, but many of them don't
work from VS Code. On e2-dogfood, five that the listing returns
(deepseek-v4-flash-0731, deepseek-v4-pro-0813, glm-5-3, glm-5-3-flash,
kimi-k3) fail with "Could not route request" on both Chat Completions and
Responses, kimi-k3-neo returns 404, and gpt-oss-120b stops without calling a
tool when Copilot sends its full tool list.

Keep the models Copilot CLI offers: Claude and GPT, using the same rule
(copilot_exclusion_reason), which leaves out gpt-oss and the other open
models.

Co-authored-by: Isaac <no-reply@databricks.com>
@xi-db
xi-db force-pushed the xi-db/stack/vscode-configure branch from 3904468 to 8b22c4c Compare October 29, 2026 10:26
xi-db and others added 5 commits October 29, 2026 10:33
Both stream readers stopped when the connection closed, so a stream cut off
mid-answer (a dropped connection, a proxy timeout) reached Copilot Chat as a
finished reply. Only an output-token stop was reported: a content-filter or
any other early stop was ignored, and the user got a truncated answer with
no reason.

A reply now counts as done only after the gateway says the model finished:
response.completed on Responses, finish_reason "stop" or "tool_calls" on
Chat Completions. Any other stop reason fails with a message for it, and a
stream that ends without a final event fails as cut off.

Co-authored-by: Isaac <no-reply@databricks.com>
The picker listed models by their service ids, e.g.
system.ai.claude-sonnet-5-5. ug configure now writes a name for each
model with model_picker_label, the helper Codex's picker already uses
("Claude Sonnet 5.5", "GPT 6.1 Sol"), and the extension shows it. As in
Codex, two models with the same label keep their full ids. The id still
goes to the gateway, and settings without a name show the id as before.

Co-authored-by: Isaac <no-reply@databricks.com>
The build copied vscode-extension/media/databricks.svg next to the bundle,
so the repo held two identical copies. The SVG has no build step and only
the shipped extension uses it, so keep it only in
src/ucode/vscode/extension/media/ and stop copying it. package.json still
needs both copies (npm reads one, VS Code the other); mark the shipped copy
as generated, like the bundle.

Co-authored-by: Isaac <no-reply@databricks.com>
Review fixes for the ug side of VS Code:

- A `code` command that hangs or can't start raised TimeoutExpired or
  OSError, which configure and revert didn't catch. It now fails like any
  other `code` error, with a warning.
- A VS Code failure in `ug revert` no longer stops the Claude, Codex and
  Pi restores that run after it; it shows as "failed".
- An empty model listing (a failed request, or a workspace without Claude
  or GPT models) wrote `unityGateway.models: []` over a working setup and
  printed "No models for VS Code: None". Configure now leaves VS Code
  unchanged and says why.
- `ug revert` ran `code --list-extensions` for everyone. It now runs `code`
  only when ug's settings are there, since configure writes them before it
  installs.
- `--dry-run` set VS Code up from the saved state, which a dry run doesn't
  update: a KeyError on a fresh machine, or the previous workspace. A dry
  run now leaves VS Code alone.
- Settings files that can't be read or written warn instead of raising.
- The macOS lookup test no longer depends on whether this Mac has VS Code
  in /Applications.

Co-authored-by: Isaac <no-reply@databricks.com>
… extension

Review fixes for the extension:

- Images a tool returns (a screenshot, an image file it read) were dropped:
  tool results were flattened to text. Responses now sends them inside the
  function_call_output. On Chat Completions the gateway rejects images in a
  tool message, so they go in the user message right after the results.
- An error event in a Chat Completions stream ({"error": ...}) was ignored;
  it now fails the reply with the gateway's message.
- Responses tool calls are held back until response.completed, so a
  cut-off stream can't start a tool, as on Chat Completions.
- After a second 401 the message said to run `databricks auth login`, which
  is wrong for PAT and custom OAuth setups. It now points to
  `ug configure --agents vscode`, which signs in whichever way ug is set up.
- The shipped package.json no longer carries the repo's build scripts and
  dev dependencies.
- The provider test now checks the User-Agent each request sends.

Co-authored-by: Isaac <no-reply@databricks.com>
@xi-db

xi-db commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks, Polly. Where each item stands (all fixed in this PR):

  1. [P1] VS Code failures interrupt unrelated restores: fixed in 918f4fa. Revert wraps the VS Code step the way it wraps Desktop's. A failure prints a warning and shows as failed, and the Claude, Codex and Pi restores still run. The new test is tests/test_cli.py::TestRevert::test_vscode_revert_failure_is_reported_and_the_rest_still_reverts.
  2. [P2] Failed discovery erases a working catalog: fixed in 918f4fa. An empty listing now leaves VS Code unchanged, so it neither wipes the old models nor pairs them with a new workspace.
  3. [P2] Images in tool results are lost: fixed in 1d76882.
    • Responses sends them inside function_call_output.
    • On Chat Completions the gateway rejects images in a tool message (400: Input tag 'image_url' found using 'type' does not match ...), so they go in the user message right after the results.
    • Checked in VS Code: Claude Haiku 5.5 and GPT 6 Luna each read a PNG through Copilot's copilot_viewImage and named its colour.
  4. [P2] In-stream error envelopes: fixed in 1d76882. An error event fails the reply with its message.
  5. [P2] Unfinished streams can dispatch tool calls:
    • Chat Completions has required a finish_reason before releasing calls since 7867ee4.
    • Responses now holds calls until response.completed (1d76882).

From the non-blocking notes: the provider test now asserts the User-Agent each request sends (1d76882).

This comment was written by Isaac.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ug-review Run the automated UG review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant