From 2f183c4cadf6e2d266ad9bccd581ca04e236d747 Mon Sep 17 00:00:00 2001 From: yetone Date: Mon, 5 Oct 2026 01:21:11 +0800 Subject: [PATCH] =?UTF-8?q?agent:=20Cursor=20Private=20Inference's=20row?= =?UTF-8?q?=20picks=20which=20of=20magpie's=20models=20its=20own=20model?= =?UTF-8?q?=20picker=20lists,=20as=20Claude=20Desktop's=20does=20(mamba=20?= =?UTF-8?q?on=20Discord:=20=E5=A6=82=E6=9E=9C=E6=98=AF=E4=BB=A5=E8=BF=99?= =?UTF-8?q?=E7=A7=8D=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E6=8E=A5=E5=85=A5=E7=9A=84=E8=AF=9D=EF=BC=8C=E9=80=89?= =?UTF-8?q?=E4=B8=8D=E4=BA=86=E6=A8=A1=E5=9E=8B=E5=9C=A8=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98)=20=E2=80=94=20its=20picker=20is=20the=20gateway's=20?= =?UTF-8?q?/v1/models=20as=20its=20key=20(magpie-cursor-local)=20is=20show?= =?UTF-8?q?n=20them,=20which=20already=20left=20out=20the=20models=20taken?= =?UTF-8?q?=20out=20for=20it=20(settings.HiddenModels),=20but=20its=20row?= =?UTF-8?q?=20had=20no=20way=20to=20take=20any=20out:=20no=20field=20of=20?= =?UTF-8?q?its=20picks=20among=20the=20catalog,=20so=20it=20had=20no=20mod?= =?UTF-8?q?el=20line=20or=20list.=20Agent.ListsModels=20marks=20an=20agent?= =?UTF-8?q?=20whose=20menu=20is=20that=20list;=20the=20row's=20line=20coun?= =?UTF-8?q?ts=20its=20models=20for=20it=20(agentModelCount=20takes=20the?= =?UTF-8?q?=20agent),=20and=20in=20the=20window=20and=20the=20tray=20panel?= =?UTF-8?q?=20the=20connected=20row=20has=20the=20Models=20button=20beside?= =?UTF-8?q?=20the=20launch=20square=20(MENU=5FFROM=5FLIST),=20which=20open?= =?UTF-8?q?s=20its=20model=20list=20to=20tick=20several=20at=20once;=20it?= =?UTF-8?q?=20says=20to=20quit=20and=20reopen=20it=20after=20a=20change.?= =?UTF-8?q?=20cursor-local-models.test.cjs=20(WebKit,=20en=20and=20zh,=20w?= =?UTF-8?q?indow=20and=20panel:=20the=20row's=20words,=20the=20button=20be?= =?UTF-8?q?side=20the=20launch=20square,=20a=20model=20taken=20out=20and?= =?UTF-8?q?=20put=20back=20posted,=20no=20scroll,=20no=20, and no click scrolls the +// page. In English and Chinese. No backend: faked here. +const assert = require("node:assert/strict"); +const fs = require("node:fs/promises"); +const path = require("node:path"); +const { test } = require("node:test"); +const { chromium, webkit } = require("playwright"); + +const assets = path.resolve(__dirname, "../assets"); +const LAUNCH = "CURSOR_LOCAL_AGENT_BASE_URL=http://127.0.0.1:3425/v1 CURSOR_LOCAL_AGENT_API_KEY=magpie-cursor-local '/Applications/Cursor.app/Contents/MacOS/Cursor'"; + +function fixture(lang) { + const list = [ + { id: "opencode-go/deepseek-v4.1-flash", name: "DeepSeek V4.1 Flash", group: "OpenCode Go", icon: "opencode" }, + { id: "grok-2/grok-4.6", name: "grok-4.6", group: "Grok", icon: "generic" }, + { id: "relay/glm-5", name: "GLM-5", group: "Relay", icon: "generic" }, + { id: "relay/kimi-k3", name: "Kimi K3", group: "Relay", icon: "generic", hidden: true }, + ]; + const posts = []; + const count = () => { + const on = list.filter((m) => !m.hidden); + const by = []; + for (const m of on) { + let g = by.find((x) => x.name === m.group); + if (!g) by.push(g = { name: m.group, icon: m.icon, n: 0 }); + g.n++; + } + return { shown: on.length, listed: list.length, by, names: on.slice(0, 3).map((m) => m.name) }; + }; + const state = () => ({ + agents: [{ + id: "cursor-local", name: "Cursor Private Inference", path: "", icon: "cursor", launch: LAUNCH, wired: true, + fields: [ + { key: "provider", label: "provider", value: "magpie", options: [{ value: "magpie", label: "magpie", icon: "magpie" }] }, + ], + models: count(), + }], + profiles: [], settings: { lang, theme: "light" }, + }); + async function serve(route) { + const req = route.request(), url = new URL(req.url()); + const json = (data) => route.fulfill({ json: data }); + if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:true};` }); + if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" }); + if (url.pathname === "/api/state") return json(state()); + if (url.pathname === "/api/agent-models/cursor-local") { + if (req.method() === "POST") { + const { hidden } = req.postDataJSON(); + posts.push(hidden); + for (const m of list) m.hidden = hidden.includes(m.id); + return json({ models: list, count: count() }); + } + return json({ models: list }); + } + if (url.pathname === "/api/usage/quotas") return json([]); + if (url.pathname === "/api/groups") return json({ groups: [] }); + if (url.pathname === "/api/agents/cli") return json({ agents: {}, pending: false }); + if (url.pathname === "/api/providers") return json({ providers: [], gateway: { running: true } }); + if (url.pathname === "/api/plugins") return json({ plugins: [] }); + if (url.pathname.startsWith("/api/")) return json({}); + const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname); + const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)]; + await route.fulfill({ body: await fs.readFile(file), contentType }); + } + return { serve, posts }; +} + +const W = { + en: { said: "Connected · 3 models in Cursor Private Inference's model menu", after: "Connected · 2 models in Cursor Private Inference's model menu", title: "Cursor Private Inference's model list", restart: "quit and reopen it", details: "Details" }, + zh: { said: "已接入 · Cursor Private Inference 的模型菜单里有 3 个模型", after: "已接入 · Cursor Private Inference 的模型菜单里有 2 个模型", title: "Cursor Private Inference 的模型列表", restart: "退出并重新打开", details: "详情" }, +}; +const cd = '.row.agent[data-id="cursor-local"]'; + +for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) { + for (const lang of ["en", "zh"]) { + for (const where of ["window", "panel"]) { + test(`${engine} ${lang} ${where}: Cursor Private Inference's row picks the models its picker lists`, async (t) => { + const w = W[lang]; + const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" })); + const page = await browser.newPage({ viewport: where === "panel" ? { width: 420, height: 640 } : { width: 1100, height: 700 } }); + page.setDefaultTimeout(5000); + const errors = []; + page.on("pageerror", (e) => errors.push(e.message)); + const fx = fixture(lang); + await page.route("**/*", fx.serve); + t.after(() => browser.close()); + await page.goto("http://magpie.test/" + (where === "panel" ? "?mode=panel" : "")); + await page.locator(cd).waitFor(); + + let menu; + if (where === "window") { + assert.equal(await page.locator(`${cd} .ag-st-t`).textContent(), w.said); + assert.equal(await page.locator(`${cd} .ag-link`).textContent(), w.details); + assert.equal(await page.locator(`${cd} > .field.launch`).count(), 1, "the launch command's square is gone"); + menu = page.locator(`${cd} > .ag-menu`); + } else { + assert.equal(await page.locator(`${cd} .ag-sum .vt`).textContent(), "DeepSeek V4.1 Flash, grok-4.6 +1"); + await page.locator(`${cd} .ag-sum`).click(); + menu = page.locator(`${cd} .ag-open .ag-menu`); + await page.waitForFunction((sel) => document.querySelector(sel)?.getBoundingClientRect().height > 0, `${cd} .ag-open .ag-menu`); + assert.equal(await page.locator(`${cd} .ag-conn-said`).textContent(), w.said); + } + assert.equal(await menu.count(), 1, "no model button"); + assert.equal(await menu.locator(".v").textContent(), "DeepSeek V4.1 Flash, grok-4.6 +1"); + assert.ok((await menu.getAttribute("title")).includes(w.restart), await menu.getAttribute("title")); + + const y = await page.evaluate(() => scrollY); + await menu.click(); + const box = page.locator(".am-pop:not(.leaving)"); + await box.waitFor(); + assert.equal(await box.locator(".am-t").textContent(), w.title); + const rows = await box.locator(".am-mr").evaluateAll((es) => es.map((e) => [e.querySelector(".n").textContent, e.getAttribute("aria-checked")])); + assert.deepEqual(rows, [["DeepSeek V4.1 Flash", "true"], ["grok-4.6", "true"], ["GLM-5", "true"], ["Kimi K3", "false"]]); + + // several at once: GLM-5 out, the list stays open, the row says so + await box.locator(".am-mr", { hasText: "GLM-5" }).click(); + await page.waitForFunction(() => document.querySelector(".ag-menu .v")?.textContent === "DeepSeek V4.1 Flash, grok-4.6"); + await page.waitForTimeout(150); + assert.equal(fx.posts.length, 1); + assert.deepEqual(fx.posts.at(-1).sort(), ["relay/glm-5", "relay/kimi-k3"]); + assert.equal(await box.count(), 1, "the list closed after one pick"); + assert.equal(await menu.locator(".v").textContent(), "DeepSeek V4.1 Flash, grok-4.6"); + const said = where === "window" ? `${cd} .ag-st-t` : `${cd} .ag-conn-said`; + assert.equal(await page.locator(said).textContent(), w.after); + if (where === "panel") assert.equal(await page.locator(`${cd} .ag-sum .vt`).textContent(), "DeepSeek V4.1 Flash, grok-4.6"); + // and Kimi K3 back in + await box.locator(".am-mr", { hasText: "Kimi K3" }).click(); + await page.waitForFunction(() => document.querySelector(".ag-menu .v")?.textContent === "DeepSeek V4.1 Flash, grok-4.6 +1"); + await page.waitForTimeout(150); + assert.equal(fx.posts.length, 2); + assert.deepEqual(fx.posts.at(-1), ["relay/glm-5"]); + assert.equal(await page.locator(said).textContent(), w.said); + + assert.equal(await page.evaluate(() => scrollY), y, "a click scrolled the page"); + assert.equal(await page.locator("select").count(), 0, "a native select"); + assert.deepEqual(errors, []); + }); + } + } +}