agent: VS Code Insiders is an agent of its own, its Chat given magpie's models in Code - Insiders/User as VS Code's is in Code/User (wani on Discord)
vscode.go's agent is now one per build (vscodeKind): VS Code as it was, and VS Code Insiders, id vscode-insiders, its User folder Code - Insiders beside Code on the Mac, Windows and Linux, its binary code-insiders. Insiders' chat names itself GitHubCopilotChat as Stable's does, so it has no UA of its own: its models send the token magpie-vscode-insiders, which the gateway reads first, and its requests are its own in Usage while Stable's stay Stable's. Connecting one writes nothing of the other's. TestVSCodeInsiders fails without the change (unknown agent "vscode-insiders"). go vet, go build (GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
This commit is contained in:
1 parent
c6e318e1d2
commit
1330b9660e
5 files changed
+109
-19
No files matched your search
@@ -222,7 +222,7 @@ Write your instructions, MCP servers and skills once. magpie writes them into ea
|
||||
<table>
|
||||
<tr><td>
|
||||
|
||||
Claude Code · Claude Desktop · Codex · Gemini CLI · OpenCode · OpenChamber · MiMo Code · Pi · Aside · OmO · Goose · Cursor CLI · Zed · VS Code Chat · JetBrains Air · Copilot CLI · Crush · DeepSeek Harness · Command Code · fx · oh-my-pi · Devin · Hermes Agent · Mister Morph · Kimi Code · Muse Code · Empryo · MiniMax Code · Droid · Cline · Qoder · Qoder CN · Grok Build · ZCode · WorkBuddy · T3 Code · OpenHanako · AtomCode · Alma
|
||||
Claude Code · Claude Desktop · Codex · Gemini CLI · OpenCode · OpenChamber · MiMo Code · Pi · Aside · OmO · Goose · Cursor CLI · Zed · VS Code Chat · VS Code Insiders · JetBrains Air · Copilot CLI · Crush · DeepSeek Harness · Command Code · fx · oh-my-pi · Devin · Hermes Agent · Mister Morph · Kimi Code · Muse Code · Empryo · MiniMax Code · Droid · Cline · Qoder · Qoder CN · Grok Build · ZCode · WorkBuddy · T3 Code · OpenHanako · AtomCode · Alma
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -90,6 +90,7 @@ line; agents connected to magpie lose it when it quits.
|
||||
| Cursor CLI | `~/.cursor/cli-config.json` | model |
|
||||
| Zed | `~/.config/zed/settings.json` (`$XDG_CONFIG_HOME` on Linux, `%APPDATA%\Zed` on Windows) | model (a `magpie` OpenAI-compatible provider; its catalog in Zed's picker) |
|
||||
| VS Code (Chat) | `~/Library/Application Support/Code/User/settings.json` + `chatLanguageModels.json` (`~/.config/Code/User` on Linux, `%APPDATA%\Code\User` on Windows) | model (`chat.defaultModel`; a `magpie` Custom Endpoint group, its catalog in Chat's model picker; VS Code 1.122+, no Copilot sign-in or key needed). Each profile's own pair under `User/profiles/<id>/` (listed in `globalStorage/storage.json`) gets the same, unless the profile uses the default's |
|
||||
| VS Code Insiders (Chat) | the same files under `Code - Insiders/User` in place of `Code/User` | as VS Code, a row of its own; its models send the token `magpie-vscode-insiders`, since its chat's User-Agent is VS Code's |
|
||||
| JetBrains Air | `acp.json` in `~/Library/Application Support/JetBrains/Air` (`~/.config/JetBrains/Air` on Linux, `%APPDATA%\JetBrains\Air` on Windows) + `magpie-opencode.json` beside it | model (a `Magpie` ACP agent: OpenCode's `opencode acp` on magpie's provider alone, its models and routing groups in Air's model menu; needs OpenCode installed) |
|
||||
| Copilot CLI | `~/.copilot/settings.json` | model |
|
||||
| Crush | `~/.config/crush/crush.json` | large, small |
|
||||
|
||||
@@ -80,6 +80,7 @@ func All() []*Agent {
|
||||
cursorLocal(),
|
||||
zed(home, cfg),
|
||||
vscode(home, cfg),
|
||||
vscodeInsidersAgent(home, cfg),
|
||||
air(home, cfg),
|
||||
copilot(home),
|
||||
crush(home, cfg),
|
||||
|
||||
+50
-18
@@ -43,9 +43,39 @@ const vscodeUA = "githubcopilotchat"
|
||||
// vscodeGroup is how magpie's group is found among the user's.
|
||||
var vscodeGroup = map[string]string{"vendor": "customendpoint", "name": magpieID}
|
||||
|
||||
// vscodeKind is one build of VS Code, each with its own User folder, its
|
||||
// own app and its own row: VS Code, and VS Code Insiders beside it (wani on
|
||||
// Discord), whose chat is the same and is set up the same way.
|
||||
type vscodeKind struct {
|
||||
id, name, folder, bin string
|
||||
aliases []string
|
||||
// ua is what its chat's User-Agent begins with: Stable is told by it.
|
||||
// Insiders' chat names itself the same, so it is told by its token
|
||||
// instead (gateway.TokenFor), which its models send.
|
||||
ua []string
|
||||
}
|
||||
|
||||
var (
|
||||
vscodeStable = vscodeKind{id: "vscode", name: "VS Code", folder: "Code", bin: "code", aliases: []string{"vs-code", "copilot-chat", "vscode-chat"}, ua: []string{vscodeUA}}
|
||||
vscodeInsiders = vscodeKind{id: "vscode-insiders", name: "VS Code Insiders", folder: "Code - Insiders", bin: "code-insiders", aliases: []string{"vs-code-insiders", "code-insiders"}}
|
||||
)
|
||||
|
||||
// token is the bearer token its models send: Stable's the gateway's own,
|
||||
// as it always was.
|
||||
func (k vscodeKind) token() string {
|
||||
if k.ua != nil {
|
||||
return gateway.Token
|
||||
}
|
||||
return gateway.TokenFor(k.id)
|
||||
}
|
||||
|
||||
// VS Code keeps its User folder under Application Support on macOS, Roaming
|
||||
// AppData on Windows and XDG on Linux.
|
||||
func vscode(home, cfg string) *Agent {
|
||||
// AppData on Windows and XDG on Linux, Insiders' beside it.
|
||||
func vscode(home, cfg string) *Agent { return vscodeOf(vscodeStable, home, cfg) }
|
||||
|
||||
func vscodeInsidersAgent(home, cfg string) *Agent { return vscodeOf(vscodeInsiders, home, cfg) }
|
||||
|
||||
func vscodeOf(k vscodeKind, home, cfg string) *Agent {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
cfg = filepath.Join(home, "Library", "Application Support")
|
||||
@@ -55,13 +85,15 @@ func vscode(home, cfg string) *Agent {
|
||||
cfg = filepath.Join(home, "AppData", "Roaming")
|
||||
}
|
||||
}
|
||||
return vscodeAt(filepath.Join(cfg, "Code", "User"))
|
||||
return vscodeKindAt(k, filepath.Join(cfg, k.folder, "User"))
|
||||
}
|
||||
|
||||
func vscodeAt(dir string) *Agent {
|
||||
func vscodeAt(dir string) *Agent { return vscodeKindAt(vscodeStable, dir) }
|
||||
|
||||
func vscodeKindAt(k vscodeKind, dir string) *Agent {
|
||||
path := filepath.Join(dir, "settings.json")
|
||||
models := filepath.Join(dir, "chatLanguageModels.json")
|
||||
key := func(p string) string { return "vscode:" + p + ":model" }
|
||||
key := func(p string) string { return k.id + ":" + p + ":model" }
|
||||
getAt := func(p string) string { v, _ := edit.GetJSON(p, vscodeDefault); return v }
|
||||
get := func() string { return getAt(path) }
|
||||
ours := func() (string, bool) { return edit.GetJSONItem(models, vscodeGroup) }
|
||||
@@ -102,7 +134,7 @@ func vscodeAt(dir string) *Agent {
|
||||
}
|
||||
// setGroup writes magpie's group in the default's and each profile's
|
||||
setGroup := func(lms []string) error {
|
||||
v := vscodeGroupJSON()
|
||||
v := vscodeGroupJSON(k)
|
||||
for _, lm := range append([]string{models}, lms...) {
|
||||
if cur, ok := edit.GetJSONItem(lm, vscodeGroup); ok && sameJSON(cur, v) {
|
||||
continue
|
||||
@@ -114,9 +146,9 @@ func vscodeAt(dir string) *Agent {
|
||||
return nil
|
||||
}
|
||||
return atomic(&Agent{
|
||||
ID: "vscode", Name: "VS Code", Icon: "vscode", Aliases: []string{"vs-code", "copilot-chat", "vscode-chat"}, Spelled: prefixed,
|
||||
Bin: "code", Dir: dir, Path: path,
|
||||
UA: []string{vscodeUA},
|
||||
ID: k.id, Name: k.name, Icon: "vscode", Aliases: k.aliases, Spelled: prefixed,
|
||||
Bin: k.bin, Dir: dir, Path: path,
|
||||
UA: k.ua,
|
||||
detect: func() bool {
|
||||
if Taken(dir) {
|
||||
return false
|
||||
@@ -124,12 +156,12 @@ func vscodeAt(dir string) *Agent {
|
||||
if isDir(dir) {
|
||||
return true
|
||||
}
|
||||
bin, err := exec.LookPath("code")
|
||||
bin, err := exec.LookPath(k.bin)
|
||||
return err == nil && vscodeCodeBinary(bin)
|
||||
},
|
||||
Notice: func() string {
|
||||
if joined() {
|
||||
return "magpie's models are in VS Code's Chat model picker, under magpie, in each of its profiles (VS Code 1.122 or later). If they don't show, run Developer: Reload Window in VS Code."
|
||||
return "magpie's models are in " + k.name + "'s Chat model picker, under magpie, in each of its profiles (VS Code 1.122 or later). If they don't show, run Developer: Reload Window in " + k.name + "."
|
||||
}
|
||||
return ""
|
||||
},
|
||||
@@ -166,10 +198,10 @@ func vscodeAt(dir string) *Agent {
|
||||
if !ok || !gjson.Get(g, "models.0").Exists() {
|
||||
return ""
|
||||
}
|
||||
return wiringOff("VS Code", models, func(k string) (string, bool) {
|
||||
return wiringOff(k.name, models, func(k string) (string, bool) {
|
||||
r := gjson.Get(g, "models.0."+k)
|
||||
return r.String(), r.Exists()
|
||||
}, "url", vscodeURL(), "requestHeaders.Authorization", "Bearer "+gateway.Token)
|
||||
}, "url", vscodeURL(), "requestHeaders.Authorization", "Bearer "+k.token())
|
||||
},
|
||||
Fields: []Field{{
|
||||
Key: "model", Label: "model", Get: model,
|
||||
@@ -215,7 +247,7 @@ func vscodeAt(dir string) *Agent {
|
||||
if v := cur["model"]; v != "" && v != "auto" && !usesMagpie(v) {
|
||||
own = append(own, Option{Value: v, Icon: modelIcon("", v)})
|
||||
}
|
||||
return append(group("VS Code", own), viaMagpie("vscode", magpieID+"/")...)
|
||||
return append(group(k.name, own), viaMagpie(k.id, magpieID+"/")...)
|
||||
},
|
||||
}},
|
||||
}, path, models, stashPath())
|
||||
@@ -300,10 +332,10 @@ func vscodeProfiles(dir string) (settings, models []string) {
|
||||
func vscodeURL() string { return gatewayV1() + "/chat/completions" }
|
||||
|
||||
// vscodeGroupJSON is magpie's group in chatLanguageModels.json: the catalog
|
||||
// as VS Code's chat is shown it.
|
||||
func vscodeGroupJSON() map[string]any {
|
||||
// as the chat of the VS Code k is shown it.
|
||||
func vscodeGroupJSON(k vscodeKind) map[string]any {
|
||||
list := []any{}
|
||||
for _, m := range magpieModels("vscode") {
|
||||
for _, m := range magpieModels(k.id) {
|
||||
context := m.Context
|
||||
if context == 0 {
|
||||
context = 128000 // what VS Code takes a model of unknown window for
|
||||
@@ -320,7 +352,7 @@ func vscodeGroupJSON() map[string]any {
|
||||
"id": m.ID, "name": m.Name, "url": vscodeURL(),
|
||||
"toolCalling": true, "vision": m.Images,
|
||||
"contextWindow": context, "maxOutputTokens": output,
|
||||
"requestHeaders": map[string]string{"Authorization": "Bearer " + gateway.Token},
|
||||
"requestHeaders": map[string]string{"Authorization": "Bearer " + k.token()},
|
||||
}
|
||||
if len(m.Efforts) > 0 {
|
||||
entry["supportsReasoningEffort"] = m.Efforts
|
||||
|
||||
@@ -233,3 +233,59 @@ func TestVSCodeUA(t *testing.T) {
|
||||
t.Fatalf("%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// VS Code Insiders is its own row (wani on Discord): its User folder is
|
||||
// "Code - Insiders" beside VS Code's "Code", connected it writes only there,
|
||||
// and its models send a token of its own, since its chat's User-Agent is
|
||||
// Stable's; VS Code's files are left alone
|
||||
func TestVSCodeInsiders(t *testing.T) {
|
||||
home := syncHome(t)
|
||||
ins, err := Find("vscode-insiders")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st, err := Find("vscode")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ins.Name != "VS Code Insiders" || ins.Bin != "code-insiders" || filepath.Base(filepath.Dir(ins.Dir)) != "Code - Insiders" || filepath.Base(ins.Dir) != "User" ||
|
||||
filepath.Dir(filepath.Dir(ins.Dir)) != filepath.Dir(filepath.Dir(st.Dir)) || ins.UA != nil {
|
||||
t.Fatalf("insiders: %+v\nstable dir %s", ins, st.Dir)
|
||||
}
|
||||
if !strings.HasPrefix(ins.Dir, home) {
|
||||
t.Fatalf("insiders dir %s outside %s", ins.Dir, home)
|
||||
}
|
||||
for _, n := range []string{"code-insiders", "vs-code-insiders"} {
|
||||
if a, err := Find(n); err != nil || a.ID != "vscode-insiders" {
|
||||
t.Fatalf("Find(%q): %v %v", n, a, err)
|
||||
}
|
||||
}
|
||||
writeFile(t, ins.Path, "{}\n")
|
||||
writeFile(t, st.Path, `{"chat.defaultModel": "gpt-5"}`+"\n")
|
||||
if !ins.Detected() {
|
||||
t.Fatal("not detected")
|
||||
}
|
||||
if err := ins.Connect(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g, ok := edit.GetJSONItem(filepath.Join(ins.Dir, "chatLanguageModels.json"), vscodeGroup)
|
||||
if !ok || gjson.Get(g, "models.0.requestHeaders.Authorization").String() != "Bearer magpie-vscode-insiders" || !ins.Wired() || ins.Check() != "" {
|
||||
t.Fatalf("connected: %s %v %q", g, ins.Wired(), ins.Check())
|
||||
}
|
||||
if readFile(st.Path) != `{"chat.defaultModel": "gpt-5"}`+"\n" || isFile(filepath.Join(st.Dir, "chatLanguageModels.json")) || st.Wired() {
|
||||
t.Fatalf("VS Code touched:\n%s", readFile(st.Path))
|
||||
}
|
||||
// its requests are its own, Stable's still Stable's
|
||||
if got := usage.AgentOf("vscode-insiders"); got != "vscode-insiders" {
|
||||
t.Fatalf("token: %q", got)
|
||||
}
|
||||
if got := usage.AgentOf("GitHubCopilotChat/0.69.0"); got != "vscode" {
|
||||
t.Fatalf("UA: %q", got)
|
||||
}
|
||||
if err := ins.Disconnect(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := edit.GetJSONItem(filepath.Join(ins.Dir, "chatLanguageModels.json"), vscodeGroup); ok || ins.Wired() {
|
||||
t.Fatal("disconnected, magpie's group kept")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user