diff --git a/README.md b/README.md
index 2d6f609b..8ee1e7c4 100644
--- a/README.md
+++ b/README.md
@@ -222,7 +222,7 @@ Write your instructions, MCP servers and skills once. magpie writes them into ea
|
-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
|
diff --git a/docs/reference.md b/docs/reference.md
index 77172a86..a4204e24 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -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//` (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 |
diff --git a/internal/agent/agents.go b/internal/agent/agents.go
index cc95a77a..5c84332c 100644
--- a/internal/agent/agents.go
+++ b/internal/agent/agents.go
@@ -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),
diff --git a/internal/agent/vscode.go b/internal/agent/vscode.go
index 58d8498c..c73ef24b 100644
--- a/internal/agent/vscode.go
+++ b/internal/agent/vscode.go
@@ -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
diff --git a/internal/agent/vscode_test.go b/internal/agent/vscode_test.go
index 5d53abd3..ccc319d6 100644
--- a/internal/agent/vscode_test.go
+++ b/internal/agent/vscode_test.go
@@ -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")
+ }
+}