diff --git a/devlog/_plan/260902_cursor_bundle_effort_table/041_wp4_screenshot.png b/devlog/_plan/260902_cursor_bundle_effort_table/041_wp4_screenshot.png new file mode 100644 index 0000000000..59a5a941e4 Binary files /dev/null and b/devlog/_plan/260902_cursor_bundle_effort_table/041_wp4_screenshot.png differ diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 888bfac8ea..f0eb85fcee 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -2391,6 +2391,16 @@ export const de: Record = { "integrations.cursor.neverSeen": "Seit dem Start des Proxys ist keine Anfrage von Cursor eingegangen. Klicken Sie nach dem Speichern des Gateways in Cursor auf Refresh model list.", "integrations.cursor.models": "Was Cursor anzeigt", "integrations.cursor.modelsHint": "Cursor wählt die Reasoning-Abstufung anhand seiner eigenen Modelltabelle aus, daher kann opencodex sie nur vorhersagen. Die Kontextspalte zeigt das Standardfenster und das optionale Fenster (Cursors Max Mode).", + "integrations.cursor.ladderFromBundle": "Reasoning-Abstufungen wurden aus dem installierten Cursor-Private-Inference-Bundle {version} gelesen. Cursor legt sie fest; opencodex gibt nur dessen Tabelle wieder.", + "integrations.cursor.ladderFromStatic": "Reasoning-Abstufungen sind ein statischer Spiegel von Cursor 3.18.25 (kein lesbares Private-Inference-Bundle gefunden). Die Kontextspalte zeigt das Standardfenster und das optionale Fenster.", + "integrations.cursor.unknownVersion": "unbekannte Version", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "ein Fenster", + "integrations.cursor.noControlTitle": "Diese ID steht nicht in Cursors eingebauter Effort-Tabelle, daher zeigt Cursor keine Reasoning-Steuerung an.", + "integrations.cursor.effortRowsOne": "1 Effort-Zeile veröffentlicht", + "integrations.cursor.effortRowsMany": "{n} Effort-Zeilen veröffentlicht", + "integrations.cursor.effortRowsOff": "keine Effort-Zeilen", + "integrations.cursor.tableLessHint": "Mit — markierte Zeilen erhalten in Cursor keine Reasoning-Steuerung. Aktivieren Sie cursorEffortRows, um pro Effort einen Picker-Eintrag (id--effort) zu veröffentlichen, oder setzen Sie modelDefaultReasoningEfforts beim Provider für einen festen Standard.", "integrations.cursor.colModel": "Modell", "integrations.cursor.colReasoning": "Reasoning-Aufwand", "integrations.cursor.colContext": "Kontext", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 0be94fe83b..8c49a81248 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1567,6 +1567,16 @@ export const en = { "integrations.cursor.neverSeen": "No request from Cursor since the proxy started. After saving the gateway, press Refresh model list in Cursor.", "integrations.cursor.models": "What Cursor will show", "integrations.cursor.modelsHint": "Cursor picks the Reasoning ladder from its own model table, so opencodex can only predict it. Context lists the default and the opt-in window (Cursor's Max Mode).", + "integrations.cursor.ladderFromBundle": "Reasoning ladders read from the installed Cursor Private Inference {version} bundle. Cursor decides them; opencodex only reports its table.", + "integrations.cursor.ladderFromStatic": "Reasoning ladders are a static mirror of Cursor 3.18.25 (no readable Private Inference bundle was found). Context lists the default and the opt-in window.", + "integrations.cursor.unknownVersion": "unknown version", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "single window", + "integrations.cursor.noControlTitle": "This id is not in Cursor's built-in effort table, so Cursor shows no Reasoning control.", + "integrations.cursor.effortRowsOne": "1 effort row published", + "integrations.cursor.effortRowsMany": "{n} effort rows published", + "integrations.cursor.effortRowsOff": "no effort rows", + "integrations.cursor.tableLessHint": "Rows marked — get no Reasoning control in Cursor. Turn on cursorEffortRows to publish one picker entry per effort (id--effort), or set modelDefaultReasoningEfforts on the provider for a fixed default.", "integrations.cursor.colModel": "Model", "integrations.cursor.colReasoning": "Reasoning", "integrations.cursor.colContext": "Context", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index ee0ffe0fde..a05872c155 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -2378,6 +2378,16 @@ export const fr: Record = { "integrations.cursor.neverSeen": "Aucune requête de Cursor depuis le démarrage du proxy. Après avoir enregistré la passerelle, cliquez sur Refresh model list dans Cursor.", "integrations.cursor.models": "Ce que Cursor affichera", "integrations.cursor.modelsHint": "Cursor sélectionne le niveau de raisonnement dans sa propre table de modèles ; opencodex ne peut donc que le prévoir. La colonne Contexte indique la fenêtre par défaut et celle disponible en option (le Max Mode de Cursor).", + "integrations.cursor.ladderFromBundle": "Les niveaux de raisonnement sont lus dans le bundle Cursor Private Inference {version} installé. Cursor les décide ; opencodex ne fait que rapporter sa table.", + "integrations.cursor.ladderFromStatic": "Les niveaux de raisonnement sont un miroir statique de Cursor 3.18.25 (aucun bundle Private Inference lisible trouvé). La colonne Contexte indique la fenêtre par défaut et la fenêtre optionnelle.", + "integrations.cursor.unknownVersion": "version inconnue", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "fenêtre unique", + "integrations.cursor.noControlTitle": "Cet identifiant n'est pas dans la table d'effort intégrée de Cursor, donc Cursor n'affiche aucun contrôle de raisonnement.", + "integrations.cursor.effortRowsOne": "1 ligne d'effort publiée", + "integrations.cursor.effortRowsMany": "{n} lignes d'effort publiées", + "integrations.cursor.effortRowsOff": "aucune ligne d'effort", + "integrations.cursor.tableLessHint": "Les lignes marquées — n'ont pas de contrôle de raisonnement dans Cursor. Activez cursorEffortRows pour publier une entrée du sélecteur par effort (id--effort), ou définissez modelDefaultReasoningEfforts sur le fournisseur pour une valeur fixe.", "integrations.cursor.colModel": "Modèle", "integrations.cursor.colReasoning": "Raisonnement", "integrations.cursor.colContext": "Contexte", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 9d3dba4588..ce861c5c47 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -2412,6 +2412,16 @@ export const ja: Record = { "integrations.cursor.neverSeen": "プロキシの起動後、Cursor からのリクエストはありません。ゲートウェイを保存したら、Cursor で Refresh model list を押してください。", "integrations.cursor.models": "Cursor に表示される内容", "integrations.cursor.modelsHint": "Cursor は独自のモデルテーブルから推論レベルの段階を決めるため、opencodex が示せるのは予測のみです。コンテキスト欄にはデフォルトとオプトインのウィンドウ(Cursor の Max Mode)を表示します。", + "integrations.cursor.ladderFromBundle": "推論レベルの段階は、インストール済みの Cursor Private Inference {version} バンドルから読み取りました。決めるのは Cursor で、opencodex はその表を表示するだけです。", + "integrations.cursor.ladderFromStatic": "推論レベルの段階は Cursor 3.18.25 の静的ミラーです(読み取れる Private Inference のバンドルが見つかりません)。コンテキスト欄はデフォルトとオプトインのウィンドウを示します。", + "integrations.cursor.unknownVersion": "バージョン不明", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "単一ウィンドウ", + "integrations.cursor.noControlTitle": "この ID は Cursor 内蔵の effort 表にないため、Cursor は推論コントロールを表示しません。", + "integrations.cursor.effortRowsOne": "effort 行を 1 件公開", + "integrations.cursor.effortRowsMany": "effort 行を {n} 件公開", + "integrations.cursor.effortRowsOff": "effort 行なし", + "integrations.cursor.tableLessHint": "— の行は Cursor で推論コントロールが使えません。cursorEffortRows を有効にすると effort ごとにピッカー項目(id--effort)を公開できます。固定の既定値はプロバイダーの modelDefaultReasoningEfforts で設定します。", "integrations.cursor.colModel": "モデル", "integrations.cursor.colReasoning": "推論", "integrations.cursor.colContext": "コンテキスト", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 71ebdd8fa5..bf7fe01e24 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -2413,6 +2413,16 @@ export const ko: Record = { "integrations.cursor.neverSeen": "프록시 시작 후 Cursor 요청이 없습니다. 게이트웨이 저장 후 Cursor에서 Refresh model list를 누르세요.", "integrations.cursor.models": "Cursor에 표시될 항목", "integrations.cursor.modelsHint": "Reasoning 사다리는 Cursor 자체 모델 표가 정하므로 opencodex는 예측만 합니다. Context는 기본 창과 옵트인 창(Cursor의 Max Mode)입니다.", + "integrations.cursor.ladderFromBundle": "Reasoning 사다리는 설치된 Cursor Private Inference {version} 번들에서 읽었습니다. 사다리는 Cursor가 정하고 opencodex는 그 표를 보여줄 뿐입니다.", + "integrations.cursor.ladderFromStatic": "Reasoning 사다리는 Cursor 3.18.25의 정적 미러입니다(읽을 수 있는 Private Inference 번들을 찾지 못함). Context는 기본 창과 옵트인 창입니다.", + "integrations.cursor.unknownVersion": "버전 미상", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "단일 창", + "integrations.cursor.noControlTitle": "이 id는 Cursor 내장 effort 표에 없어서 Cursor가 Reasoning 컨트롤을 보여주지 않습니다.", + "integrations.cursor.effortRowsOne": "effort 행 1개 게시됨", + "integrations.cursor.effortRowsMany": "effort 행 {n}개 게시됨", + "integrations.cursor.effortRowsOff": "effort 행 없음", + "integrations.cursor.tableLessHint": "—로 표시된 행은 Cursor에서 Reasoning 컨트롤이 없습니다. cursorEffortRows를 켜면 effort마다 picker 항목(id--effort)을 하나씩 게시하고, 고정 기본값은 provider의 modelDefaultReasoningEfforts로 정합니다.", "integrations.cursor.colModel": "모델", "integrations.cursor.colReasoning": "추론", "integrations.cursor.colContext": "컨텍스트", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 3cb6f8e10a..19ebbf68b2 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -2414,6 +2414,16 @@ export const ru: Record = { "integrations.cursor.neverSeen": "Запросов от Cursor не было с момента запуска прокси. После сохранения параметров шлюза нажмите Refresh model list в Cursor.", "integrations.cursor.models": "Что будет отображаться в Cursor", "integrations.cursor.modelsHint": "Cursor выбирает шкалу уровней рассуждений из собственной таблицы моделей, поэтому opencodex может только предсказать её. В столбце «Контекст» указаны окно по умолчанию и дополнительное окно, доступное при включении Max Mode в Cursor.", + "integrations.cursor.ladderFromBundle": "Уровни рассуждения прочитаны из установленного бандла Cursor Private Inference {version}. Их определяет Cursor; opencodex лишь показывает его таблицу.", + "integrations.cursor.ladderFromStatic": "Уровни рассуждения — статическая копия Cursor 3.18.25 (читаемый бандл Private Inference не найден). Столбец «Контекст» показывает окно по умолчанию и опциональное окно.", + "integrations.cursor.unknownVersion": "версия неизвестна", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "одно окно", + "integrations.cursor.noControlTitle": "Этого id нет во встроенной таблице усилий Cursor, поэтому Cursor не показывает управление рассуждением.", + "integrations.cursor.effortRowsOne": "опубликована 1 строка усилия", + "integrations.cursor.effortRowsMany": "опубликовано строк усилия: {n}", + "integrations.cursor.effortRowsOff": "строк усилия нет", + "integrations.cursor.tableLessHint": "Строки с — не получают управление рассуждением в Cursor. Включите cursorEffortRows, чтобы публиковать по одной записи выбора на каждое усилие (id--effort), или задайте modelDefaultReasoningEfforts у провайдера для фиксированного значения.", "integrations.cursor.colModel": "Модель", "integrations.cursor.colReasoning": "Рассуждения", "integrations.cursor.colContext": "Контекст", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 2befb8ef27..21c70d54fd 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -2414,6 +2414,16 @@ export const tr: Record = { "integrations.cursor.neverSeen": "Proxy başlatıldığından beri Cursor'dan istek alınmadı. Ağ geçidini kaydettikten sonra Cursor'da Refresh model list düğmesine basın.", "integrations.cursor.models": "Cursor'da gösterilecekler", "integrations.cursor.modelsHint": "Cursor, akıl yürütme kademesini kendi model tablosundan seçtiği için opencodex bunu yalnızca tahmin edebilir. Bağlam sütunu varsayılan pencereyi ve isteğe bağlı pencereyi (Cursor'ın Max Mode'u) listeler.", + "integrations.cursor.ladderFromBundle": "Akıl yürütme kademeleri yüklü Cursor Private Inference {version} paketinden okundu. Bunlara Cursor karar verir; opencodex yalnızca tablosunu gösterir.", + "integrations.cursor.ladderFromStatic": "Akıl yürütme kademeleri Cursor 3.18.25'in statik bir kopyasıdır (okunabilir bir Private Inference paketi bulunamadı). Bağlam sütunu varsayılan ve isteğe bağlı pencereyi gösterir.", + "integrations.cursor.unknownVersion": "bilinmeyen sürüm", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "tek pencere", + "integrations.cursor.noControlTitle": "Bu kimlik Cursor'ın yerleşik çaba tablosunda yok, bu yüzden Cursor akıl yürütme denetimi göstermez.", + "integrations.cursor.effortRowsOne": "1 çaba satırı yayımlandı", + "integrations.cursor.effortRowsMany": "{n} çaba satırı yayımlandı", + "integrations.cursor.effortRowsOff": "çaba satırı yok", + "integrations.cursor.tableLessHint": "— ile işaretli satırlar Cursor'da akıl yürütme denetimi almaz. Her çaba için bir seçici girdisi (id--effort) yayımlamak üzere cursorEffortRows'u açın veya sabit bir varsayılan için sağlayıcıda modelDefaultReasoningEfforts ayarlayın.", "integrations.cursor.colModel": "Model", "integrations.cursor.colReasoning": "Akıl yürütme", "integrations.cursor.colContext": "Bağlam", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 19d9e4321c..588b09cac0 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2376,6 +2376,16 @@ export const zhTW: Record = { "integrations.cursor.neverSeen": "代理啟動後尚未收到 Cursor 的請求。儲存閘道後,請在 Cursor 按 Refresh model list。", "integrations.cursor.models": "Cursor 會顯示的內容", "integrations.cursor.modelsHint": "Cursor 從自己的模型表決定 Reasoning 階梯,opencodex 只能預測。Context 列出預設與可選的視窗(Cursor 的 Max Mode)。", + "integrations.cursor.ladderFromBundle": "Reasoning 階梯讀取自已安裝的 Cursor Private Inference {version} bundle。階梯由 Cursor 決定,opencodex 只是呈現它的表。", + "integrations.cursor.ladderFromStatic": "Reasoning 階梯是 Cursor 3.18.25 的靜態鏡像(找不到可讀取的 Private Inference bundle)。Context 欄列出預設與可選的視窗。", + "integrations.cursor.unknownVersion": "版本不明", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "單一視窗", + "integrations.cursor.noControlTitle": "此 id 不在 Cursor 內建的 effort 表中,因此 Cursor 不會顯示 Reasoning 控制項。", + "integrations.cursor.effortRowsOne": "已發布 1 個 effort 列", + "integrations.cursor.effortRowsMany": "已發布 {n} 個 effort 列", + "integrations.cursor.effortRowsOff": "沒有 effort 列", + "integrations.cursor.tableLessHint": "標為 — 的列在 Cursor 中沒有 Reasoning 控制項。開啟 cursorEffortRows 可為每個 effort 發布一個選擇器項目(id--effort),或在 provider 上設定 modelDefaultReasoningEfforts 作為固定預設值。", "integrations.cursor.colModel": "模型", "integrations.cursor.colReasoning": "推理", "integrations.cursor.colContext": "上下文", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 5f3d36b848..136c60b254 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -2412,6 +2412,16 @@ export const zh: Record = { "integrations.cursor.neverSeen": "代理启动后尚未收到 Cursor 的请求。保存网关设置后,请在 Cursor 中点击 Refresh model list。", "integrations.cursor.models": "Cursor 将显示的内容", "integrations.cursor.modelsHint": "Cursor 会从自身的模型表中选择推理层级,因此 opencodex 只能进行预测。“上下文”列会列出默认窗口和可选窗口(Cursor 的 Max Mode)。", + "integrations.cursor.ladderFromBundle": "推理档位读取自已安装的 Cursor Private Inference {version} 包。档位由 Cursor 决定,opencodex 只是展示它的表。", + "integrations.cursor.ladderFromStatic": "推理档位是 Cursor 3.18.25 的静态镜像(未找到可读取的 Private Inference 包)。上下文列显示默认窗口和可选窗口。", + "integrations.cursor.unknownVersion": "未知版本", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "单一窗口", + "integrations.cursor.noControlTitle": "此 id 不在 Cursor 内置的 effort 表中,因此 Cursor 不显示推理控件。", + "integrations.cursor.effortRowsOne": "已发布 1 个 effort 行", + "integrations.cursor.effortRowsMany": "已发布 {n} 个 effort 行", + "integrations.cursor.effortRowsOff": "无 effort 行", + "integrations.cursor.tableLessHint": "标为 — 的行在 Cursor 中没有推理控件。开启 cursorEffortRows 可为每个 effort 发布一个选择器条目(id--effort),或在提供商上设置 modelDefaultReasoningEfforts 作为固定默认值。", "integrations.cursor.colModel": "模型", "integrations.cursor.colReasoning": "推理", "integrations.cursor.colContext": "上下文", diff --git a/gui/src/pages/integrations/CursorIntegrationPage.tsx b/gui/src/pages/integrations/CursorIntegrationPage.tsx index 1036dc624b..48beef70b5 100644 --- a/gui/src/pages/integrations/CursorIntegrationPage.tsx +++ b/gui/src/pages/integrations/CursorIntegrationPage.tsx @@ -139,7 +139,13 @@ export default function CursorIntegrationPage({ apiBase, active }: { apiBase: st

{t("integrations.cursor.models")}

-

{t("integrations.cursor.modelsHint")}

+

+ {status.effortTable.source === "bundle" + ? t("integrations.cursor.ladderFromBundle", { + version: status.effortTable.version ?? t("integrations.cursor.unknownVersion"), + }) + : t("integrations.cursor.ladderFromStatic")} +

@@ -152,12 +158,41 @@ export default function CursorIntegrationPage({ apiBase, active }: { apiBase: st {status.models.map(model => ( - - + + ))}
{model.id}{model.reasoning ? model.reasoning.join(" · ") : "—"}{model.context ? `${formatTokens(model.context.defaultWindow, locale)} · ${formatTokens(model.context.longWindow, locale)}` : "—"} + {model.reasoning + ? model.reasoning.join(" · ") + : ( + <> + + {t("integrations.cursor.noControl")} + + {model.effortRows.length > 0 + ? ( + + {t( + model.effortRows.length === 1 + ? "integrations.cursor.effortRowsOne" + : "integrations.cursor.effortRowsMany", + { n: model.effortRows.length }, + )} + + ) + : {t("integrations.cursor.effortRowsOff")}} + + )} + {model.context ? `${formatTokens(model.context.defaultWindow, locale)} · ${formatTokens(model.context.longWindow, locale)}` : t("integrations.cursor.singleWindow")}
+ {status.models.some(model => model.tableLess) && ( +

{t("integrations.cursor.tableLessHint")}

+ )}

diff --git a/gui/src/styles-integrations.css b/gui/src/styles-integrations.css index 8b9d958059..b0c438f3a0 100644 --- a/gui/src/styles-integrations.css +++ b/gui/src/styles-integrations.css @@ -176,3 +176,4 @@ .cursor-model-table { width: 100%; border-collapse: collapse; font-size: var(--text-caption); } .cursor-model-table th, .cursor-model-table td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; } .cursor-model-table th { font-weight: var(--weight-semibold); color: var(--muted); } +.cursor-effort-rows { margin-left: .5rem; font-size: .85em; } diff --git a/gui/tests/cursor-integration-page.test.tsx b/gui/tests/cursor-integration-page.test.tsx index fdaabf9d16..1cb160518b 100644 --- a/gui/tests/cursor-integration-page.test.tsx +++ b/gui/tests/cursor-integration-page.test.tsx @@ -33,9 +33,10 @@ function payload(overrides: Partial = {}): CursorIntegr regularCursor: { installed: true, path: "/Applications/Cursor.app" }, gateway: { baseUrl: "http://127.0.0.1:10100/v1", apiKeyMode: "placeholder", placeholder: "opencodex" }, lastSeen: null, + effortTable: { source: "bundle", version: "3.18.25", families: 16 }, models: [ - { id: "gpt-5.6-sol", reasoning: ["low", "medium", "high", "xhigh"], context: { defaultWindow: 272_000, longWindow: 922_000 } }, - { id: "kimi/k3", reasoning: null, context: null }, + { id: "gpt-5.6-sol", reasoning: ["low", "medium", "high", "xhigh"], family: "gpt-5.6", tableLess: false, effortRows: [], context: { defaultWindow: 272_000, longWindow: 922_000 } }, + { id: "kimi/k3", reasoning: null, family: null, tableLess: true, effortRows: [], context: null }, ], guideUrl: "https://example.invalid/guides/cursor-private-inference/", ...overrides, @@ -230,6 +231,42 @@ test("the model table shows the reasoning ladder and both context windows", asyn expect(rows[0]).toContain("922K"); expect(rows[1]).toContain("kimi/k3"); expect(rows[1]).toContain("—"); + expect(rows[1]).toContain("single window"); +}); + +test("the ladder provenance names the installed bundle, and table-less rows get the hint", async () => { + await mount(); + const text = textOf(); + expect(text).toContain("installed Cursor Private Inference 3.18.25 bundle"); + expect(text).toContain("no effort rows"); + expect(container.querySelector("[data-cursor-tableless-hint]")).not.toBeNull(); + const marker = container.querySelector(".cursor-no-control"); + expect(marker?.getAttribute("aria-label")).toContain("not in Cursor's built-in effort table"); +}); + +test("the static mirror is named when no bundle was read, and effort rows are counted", async () => { + statusResponse = () => json(payload({ + effortTable: { source: "static", version: null, families: null }, + models: [ + { id: "gpt-5.6-sol", reasoning: ["low", "medium", "high", "xhigh"], family: null, tableLess: false, effortRows: [], context: null }, + { id: "anthropic/claude-fable-5-1", reasoning: null, family: null, tableLess: true, effortRows: ["anthropic/claude-fable-5-1--low", "anthropic/claude-fable-5-1--high"], context: null }, + { id: "cursor/kimi-k3", reasoning: null, family: null, tableLess: true, effortRows: ["cursor/kimi-k3--max"], context: null }, + ], + })); + await mount(); + const text = textOf(); + expect(text).toContain("static mirror of Cursor 3.18.25"); + expect(text).toContain("2 effort rows published"); + expect(text).toContain("1 effort row published"); +}); + +test("without a table-less row the hint paragraph is absent", async () => { + statusResponse = () => json(payload({ + models: [{ id: "gpt-5.6-sol", reasoning: ["low", "medium", "high", "xhigh"], family: "gpt-5.6", tableLess: false, effortRows: [], context: null }], + })); + await mount(); + expect(container.querySelector("[data-cursor-tableless-hint]")).toBeNull(); + expect(textOf()).not.toContain("no effort rows"); }); test("a failed read is an error notice, never a fake 'not installed'", async () => { diff --git a/gui/tests/fr-localization.test.ts b/gui/tests/fr-localization.test.ts index f1c3511e95..037b03671a 100644 --- a/gui/tests/fr-localization.test.ts +++ b/gui/tests/fr-localization.test.ts @@ -15,6 +15,7 @@ const PLACEHOLDER_RE = /\{([a-zA-Z0-9_]+)\}/g; const INTENTIONAL_ENGLISH = new Set([ // Units, symbols, protocol values, machine labels, and product names. + "integrations.cursor.noControl", "uptime.hour", "uptime.second", // "auto" is the same word in French, and it labels a machine-derived alias source rather diff --git a/gui/tests/locale-parity.test.ts b/gui/tests/locale-parity.test.ts index c6bd877265..8a718e2651 100644 --- a/gui/tests/locale-parity.test.ts +++ b/gui/tests/locale-parity.test.ts @@ -30,6 +30,8 @@ async function readDict(locale: string): Promise> { // gap. Anything *not* on this list that ships an English-identical value is treated as a stale // placeholder and fails the build. const ZH_TW_KEEP_ENGLISH: ReadonlySet = new Set([ + // A bare em dash: the "no Reasoning control" marker is a symbol, not copy. + "integrations.cursor.noControl", // API protocol/endpoint names "api.chatCompletionsEndpoint", "api.messagesEndpoint", @@ -204,6 +206,8 @@ test("every locale key set matches the English source", async () => { const CURSOR_KEEP_ENGLISH: ReadonlySet = new Set([ "integrations.tab.cursor", "integrations.cursor.title", + // Em-dash marker, identical in every locale. + "integrations.cursor.noControl", "integrations.cursor.privateInference", "integrations.cursor.baseUrl", // "API Key" is the literal field name in Cursor's gateway form.