diff --git a/src-node/ai-model-effort.js b/src-node/ai-model-effort.js new file mode 100644 index 0000000000..7877495ee0 --- /dev/null +++ b/src-node/ai-model-effort.js @@ -0,0 +1,64 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/* + * Which thinking effort, if any, a query from the AI panel sends. The panel offers only the levels the + * SDK reports for the selected model; this checks the request again against the model list this + * process has, so a level the model does not support is never sent. No effort at all is the default: + * the SDK then applies the user's saved setting or the model's own default. + */ + +const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"]; + +/** + * The effort to set on a query's options, or undefined to send none. + * + * Before the first query of this process the model list is not known yet; the panel has checked the + * level against the list it kept from an earlier run, so it is sent rather than silently dropped. A + * custom endpoint's models are not Anthropic's, so their capabilities are unknown and nothing is sent. + * + * @param {Object} request + * @param {string} [request.effort] - The level the panel asked for. + * @param {string} [request.model] - The model the query names; none for the default model. + * @param {Array} [request.models] - The SDK's supportedModels() list, when this process has it. + * @param {string} [request.resolvedDefaultModel] - The model the default resolved to last, when known. + * @param {boolean} [request.customEndpoint] - Whether the query goes to a custom endpoint. + * @return {string|undefined} A level the model supports, or undefined. + */ +function effortForQuery(request) { + const { effort, model, models, resolvedDefaultModel, customEndpoint } = request || {}; + if (!EFFORT_LEVELS.includes(effort) || customEndpoint) { + return undefined; + } + const target = model || resolvedDefaultModel; + if (!Array.isArray(models) || !models.length || !target) { + return effort; + } + const entry = models.find(function (m) { + return m && (m.value === target || m.resolvedModel === target); + }); + if (!entry || !entry.supportsEffort || !Array.isArray(entry.supportedEffortLevels)) { + return undefined; + } + return entry.supportedEffortLevels.includes(effort) ? effort : undefined; +} + +exports.EFFORT_LEVELS = EFFORT_LEVELS; +exports.effortForQuery = effortForQuery; diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 967917d9eb..5331cf9f99 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -35,6 +35,7 @@ const ImagePreview = require("./ai-image-preview"); const {buildSystemPrompt, buildEditorContextLine} = require("./ai-system-prompt"); const CliConnector = require("./ai-cli-connector"); const CliCapabilities = require("./ai-cli-capabilities"); +const ModelEffort = require("./ai-model-effort"); const NodeConnector = require("./node-connector"); exports.readImagePreview = ImagePreview.readImage; @@ -129,6 +130,12 @@ let currentSessionId = null; // best-effort, from the first live query. null until available. let cachedModelList = null; +// The model the last query sent without a model resolved to (the user's saved +// Claude Code default) and the endpoint it was resolved on, so an effort asked +// for on the default model is checked against that model, and only while the +// endpoint is the same. null until a default query has started. +let _lastResolvedDefault = null; + /** * Fetch the model list from a live Query once per process. Best-effort: * the control request can fail on older CLIs or non-streaming input — @@ -812,13 +819,14 @@ function _isAiScratchPath(filePath) { /** * Send a prompt to Claude and stream results back to the browser. - * Called from browser via execPeer("sendPrompt", {prompt, projectPath, sessionAction, model}). + * Called from browser via execPeer("sendPrompt", {prompt, projectPath, sessionAction, model, effort}). + * effort is a thinking effort level for the model, or absent to leave the default to the SDK. * * Returns immediately with a requestId. Results are sent as events: * aiProgress, aiTextStream, aiToolEdit, aiError, aiComplete */ exports.sendPrompt = async function (params) { - const { prompt, projectPath, sessionAction, model, locale, selectionContext, editorContext, + const { prompt, projectPath, sessionAction, model, effort, locale, selectionContext, editorContext, images, envOverrides, permissionMode, additionalDirectories, aiScratchDir } = params; if (typeof aiScratchDir === "string" && aiScratchDir) { _aiScratchDir = aiScratchDir; @@ -874,7 +882,7 @@ exports.sendPrompt = async function (params) { } // Run the query asynchronously — don't await here so we return requestId immediately - _runQuery(requestId, enrichedPrompt, projectPath, model, currentAbortController.signal, locale, images, envOverrides, permissionMode, additionalDirectories) + _runQuery(requestId, enrichedPrompt, projectPath, model, currentAbortController.signal, locale, images, envOverrides, permissionMode, additionalDirectories, effort) .catch(err => { console.error("[Phoenix AI] Query error:", err); }); @@ -1063,7 +1071,7 @@ exports.clearClarification = async function () { /** * Internal: run a Claude SDK query and stream results back to the browser. */ -async function _runQuery(requestId, prompt, projectPath, model, signal, locale, images, envOverrides, permissionMode, additionalDirectories) { +async function _runQuery(requestId, prompt, projectPath, model, signal, locale, images, envOverrides, permissionMode, additionalDirectories, effort) { // Sync the runtime mutable that hooks read for permission decisions — // setPermissionMode (peer) updates this same variable when the user // cycles modes mid-stream. @@ -2109,6 +2117,22 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, queryOptions.model = model; } + // Thinking effort: only a level the model supports, and none at all for + // the default, so the user's saved setting or the model's own default applies. + // The endpoint is the query's own environment, inherited variables included. + const endpoint = queryOptions.env.ANTHROPIC_BASE_URL || ""; + const effortLevel = ModelEffort.effortForQuery({ + effort: effort, + model: model, + models: cachedModelList, + resolvedDefaultModel: _lastResolvedDefault && _lastResolvedDefault.endpoint === endpoint ? + _lastResolvedDefault.model : null, + customEndpoint: !!endpoint + }); + if (effortLevel) { + queryOptions.effort = effortLevel; + } + // Resume session if we have an existing one (already cleared if sessionAction was "new") if (currentSessionId) { @@ -2260,6 +2284,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // Claude Code default. requestedModel lets the browser tell // an explicit pick apart from default resolution. if (message.type === "system" && message.subtype === "init" && message.model) { + if (!model) { + _lastResolvedDefault = { model: message.model, endpoint: endpoint }; + } nodeConnector.triggerPeer("aiSessionInfo", { model: message.model, requestedModel: model || null diff --git a/src-node/test-connection.js b/src-node/test-connection.js index 7b77607c01..f6accf7cc2 100644 --- a/src-node/test-connection.js +++ b/src-node/test-connection.js @@ -5,6 +5,7 @@ require("./test/test-ai-cli-connector"); require("./test/test-npm-node-shim"); require("./test/test-media-server"); require("./test/test-builder-hub"); +require("./test/test-ai-model-effort"); const TEST_NODE_CONNECTOR_ID = "ph_test_connector"; const nodeConnector = NodeConnector.createNodeConnector(TEST_NODE_CONNECTOR_ID, exports); diff --git a/src-node/test/test-ai-model-effort.js b/src-node/test/test-ai-model-effort.js new file mode 100644 index 0000000000..ce5563448f --- /dev/null +++ b/src-node/test/test-ai-model-effort.js @@ -0,0 +1,35 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +// Test-only connector for the thinking effort a query sends; the check is pure, so it runs in place. +const NodeConnector = require("../node-connector"); +const ModelEffort = require("../ai-model-effort"); + +/** + * @param {Object} request - As ai-model-effort's effortForQuery takes. + * @return {Promise<{effort: (string|null)}>} The level a query would send, or null for none. + */ +async function effortForQuery(request) { + const effort = ModelEffort.effortForQuery(request); + return { effort: effort === undefined ? null : effort }; +} + +exports.effortForQuery = effortForQuery; +NodeConnector.createNodeConnector("ph_test_ai_model_effort", exports); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index ce8a94640f..c6a7c804b1 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2694,6 +2694,15 @@ define({ "AI_CHAT_START_SETUP": "Set up", "AI_CHAT_START_CHAT_RUNNING": "In progress", "AI_CHAT_START_CLI_RUNNING": "Running", + "AI_CHAT_CLI_ADD_SESSION": "+ Add CLI session", + "AI_CHAT_CLI_RENAME_SESSION": "Rename session…", + "AI_CHAT_CLI_STOP_SESSION": "Stop session", + "AI_CHAT_CLI_REMOVE_SESSION": "Remove session", + "AI_CHAT_CLI_REMOVE_CONFIRM_TITLE": "Remove {0}?", + "AI_CHAT_CLI_REMOVE_CONFIRM_MSG": "This removes the session card and stops the running {0} session, including any task it is working on.", + "AI_CHAT_CLI_SESSION_OPTIONS": "Options for {0}", + "AI_CHAT_CLI_SESSION_NAME": "{0} {1}", + "AI_CHAT_CLI_SESSION_IDENTITY": "{0} · #{1}", "AI_CHAT_START_CLI_FOLDED": "Use a CLI instead: {0}", "AI_CHAT_START_CUSTOM_PROVIDER": "Custom endpoint or a provider like {0}? See {1}.", "AI_CHAT_SURPRISE_ME_USER_MSG": "Surprise me!", @@ -2992,6 +3001,22 @@ define({ "AI_CHAT_MODEL_SELECT_TITLE": "Choose the AI model for this chat", "AI_CHAT_MODE_SELECT_TITLE": "Switch between the Claude Code chat and an embedded CLI terminal", "AI_CHAT_MODEL_SWITCHED_NOTICE": "Switched to {0}. Applies from your next message; the first response may take a moment longer while the cache rebuilds.", + "AI_CHAT_EFFORT_TITLE": "Thinking effort", + "AI_CHAT_EFFORT_CURRENT": "Thinking effort: {0}", + "AI_CHAT_EFFORT_HEADING": "Thinking effort · {0}", + "AI_CHAT_EFFORT_DEFAULT": "Default", + "AI_CHAT_EFFORT_DEFAULT_LABEL": "Default effort", + "AI_CHAT_EFFORT_DEFAULT_HINT": "{0} decides how much to think", + "AI_CHAT_EFFORT_LOW": "Low", + "AI_CHAT_EFFORT_MEDIUM": "Medium", + "AI_CHAT_EFFORT_HIGH": "High", + "AI_CHAT_EFFORT_XHIGH": "Extra high", + "AI_CHAT_EFFORT_MAX": "Max", + "AI_CHAT_EFFORT_LOW_HINT": "Fastest, least thinking", + "AI_CHAT_EFFORT_MEDIUM_HINT": "Balanced speed and depth", + "AI_CHAT_EFFORT_HIGH_HINT": "Deep reasoning", + "AI_CHAT_EFFORT_XHIGH_HINT": "Deeper than high", + "AI_CHAT_EFFORT_MAX_HINT": "Most thorough, slowest", "AI_CHAT_INPUT_HINT": "Press {0} to send · {1} for new line", "AI_CHAT_BASH_CONFIRM_TITLE": "Allow command?", "AI_CHAT_BASH_ALLOW": "Allow", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 26057df577..a65a153fac 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -98,22 +98,6 @@ flex: 0 1 auto; min-width: 0; margin: 0 auto; - cursor: pointer; - padding: 2px 6px; - border-radius: 6px; - transition: background 0.15s ease; - - &:hover, - &.dropdown-open { - background: rgba(255, 255, 255, 0.06); - } - - .ai-chat-title-chevron { - flex: none; - margin-left: 5px; - font-size: 9px; - opacity: 0.5; - } } .ai-chat-title-icon { @@ -135,10 +119,8 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-weight: 400; - font-size: @label-font-size; - color: @project-panel-text-2; - line-height: 19px; + font: inherit; + color: inherit; } .ai-chat-header-actions { @@ -1361,6 +1343,51 @@ } } + .ai-cli-session-actions { + display: flex; + justify-content: flex-end; + margin: 0 0 8px; + } + + .ai-cli-card { + position: relative; + min-width: 0; + + .ai-start-row { + height: 100%; + padding-right: 36px; + } + + .ai-cli-session-more { + position: absolute; + right: 6px; + top: 6px; + padding: 0; + width: 24px; + height: 24px; + font-size: 18px; + line-height: 24px; + color: @project-panel-text-2; + background: transparent; + } + + &.ai-cli-card-missing { + .ai-start-row { padding-right: 10px; } + .ai-cli-session-more { display: none; } + } + + input.ai-cli-session-name { + position: absolute; + box-sizing: border-box; + min-height: 20px; + margin: 0; + padding: 0 2px; + font: inherit; + font-weight: 600; + z-index: 1; + } + } + /* Claude Code before it is signed in: Set up where the check mark was. */ .ai-start-chat-setup-btn { display: none; @@ -4499,11 +4526,6 @@ } } - // The info flash shares the right side: the chip stops claiming the space then. - .ai-info.ai-info-flash + .ai-usage-chip { - margin-left: 8px; - } - // The drop-up borrows the attach menu's surface. .ai-usage-pop { display: none; @@ -6990,3 +7012,237 @@ button:focus-visible { outline: 2px solid @project-panel-text-2; outline-offset: -3px; } } .ai-user-files .ai-attachment-card { width: 180px; } + +/* ── Thinking effort: a chip after the permission control, opening a slider dropdown ───── */ +@ai-effort-accent: #4285F4; + +.ai-chat-input-area .ai-chat-mode-row { + // Looks like the permission ("Auto") control it sits next to. + .ai-effort-chip { + position: relative; + margin-left: 2px; + display: flex; + align-items: center; + gap: 5px; + height: 22px; + padding: 0 6px; + border-radius: 4px; + cursor: pointer; + user-select: none; + white-space: nowrap; + + &[hidden] { + display: none; + } + + &:hover, &.ai-effort-open { + background: rgba(255, 255, 255, 0.04); + } + + // An outline bulb with a solid one over it, revealed from the bottom up to the chosen level. + .ai-effort-icon { + display: inline-grid; + font-size: 10px; + color: @project-panel-text-2; + + > i { + grid-area: ~"1 / 1"; + } + } + + .ai-effort-bulb-fill { + clip-path: ~"inset(calc(100% - var(--ai-effort-fill, 0%)) 0 0 0)"; + } + + &[data-effort-level="xhigh"] .ai-effort-icon, + &[data-effort-level="max"] .ai-effort-icon { + color: #88733e; + + .dark & { + color: #c2ae78; + } + } + + &[data-effort-level="max"] .ai-effort-icon { + filter: drop-shadow(0 0 1px rgba(175, 133, 36, 0.4)) drop-shadow(0 0 3px rgba(175, 133, 36, 0.18)); + + .dark & { + filter: drop-shadow(0 0 1px rgba(255, 218, 124, 0.55)) drop-shadow(0 0 3px rgba(238, 190, 75, 0.28)); + } + } + + .ai-effort-chip-text { + font-size: @ai-text-meta; + color: @project-panel-text-2; + line-height: 1; + } + + .ai-effort-caret { + font-size: 9px; + opacity: 0.7; + } + } + + // The input hint ("Enter to send …") flashes briefly; it gets the row's right side to itself. + .ai-info.ai-info-flash ~ .ai-usage-chip { + display: none; + } +} + +/* ── Thinking effort dropdown ─────────────────────────────────────── */ +/* The standard DropdownButton widget, like the model selector, with the slider panel as its one HTML + item (as the Tasks status dropdown does). The item itself is not a pick, so it never highlights. */ +.dropdownbutton-popup.ai-effort-dropdown-popup { + width: 270px; + max-height: 90vh; // the shared 160px cap would scroll the panel + + &.dropdown-menu li a { + padding: 6px 12px; + white-space: normal; + cursor: default; + } + + &.dropdown-menu a.selected { + background: none; + } + + .ai-effort-panel { + font-size: @ai-text-meta; + } + + .ai-effort-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; + } + + .ai-effort-heading { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 10px; + letter-spacing: 0.04em; + text-transform: uppercase; + opacity: 0.6; + } + + .ai-effort-value { + font-size: 12px; + } + + .ai-effort-controls { + display: flex; + align-items: flex-start; + gap: 10px; + } + + .ai-effort-default { + flex: none; + padding: 1px 9px; + border: 1px solid @ai-effort-accent; + border-radius: 10px; + background: transparent; + color: @ai-effort-accent; + font-size: @ai-text-meta; + cursor: pointer; + + &.active { + background: @ai-effort-accent; + color: #fff; + } + } + + .ai-effort-scale { + flex: 1; + min-width: 0; + } + + .ai-effort-slider { + display: block; + width: 100%; + height: 16px; + margin: 0; + accent-color: @ai-effort-accent; + cursor: pointer; + + &.ai-effort-dim { + // Default is not a particular stop: show an empty track while keeping it interactive. + -webkit-appearance: none; + appearance: none; + background: transparent; + + &::-webkit-slider-runnable-track { + height: 4px; + border-radius: 2px; + background: fade(@project-panel-text-2, 25%); + } + + &::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + margin-top: -6px; + opacity: 0; + } + + &::-moz-range-track { + height: 4px; + border-radius: 2px; + background: fade(@project-panel-text-2, 25%); + } + + &::-moz-range-progress { + background: transparent; + } + + &::-moz-range-thumb { + opacity: 0; + } + } + } + + // Labels sit under the slider's stops: the inset keeps them centred on the thumb's travel. + .ai-effort-ticks { + position: relative; + height: 13px; + margin: 2px 7px 0; + font-size: 10px; + opacity: 0.6; + } + + .ai-effort-tick { + position: absolute; + top: 0; + margin: 0; + padding: 0; + border: 0; + background: none; + color: inherit; + font: inherit; + cursor: pointer; + transform: translateX(-50%); + white-space: nowrap; + + &:hover, &:focus-visible { + color: @ai-effort-accent; + } + } + + .ai-effort-hint { + margin-top: 8px; + opacity: 0.6; + } +} + +.ai-chat-panel.ai-mode-cli .ai-effort-chip { + display: none; +} + +/* The panel's dropdown triggers share the model picker's chevron. */ +.ai-dropdown-chevron { + font-size: 9px; + opacity: 0.7; + margin-left: 5px; +} diff --git a/src/styles/Extn-RecentProjects.less b/src/styles/Extn-RecentProjects.less index 3ee390a38d..9ba1dfe4e9 100644 --- a/src/styles/Extn-RecentProjects.less +++ b/src/styles/Extn-RecentProjects.less @@ -44,6 +44,31 @@ } } +// Reuse the Files project switcher's quiet trigger for sidebar dropdowns. Keep the +// generic .btn class off these controls: its dark hover fill overrides btn-alt-quiet. +.sidebar-project-dropdown { + display: inline-flex; + align-items: center; + appearance: none; + padding: 2px 5px; + background-color: transparent; + color: @project-panel-text-2; + font-family: @sansFontFamily; + font-size: 15px; + font-weight: 400; + line-height: 18px; + letter-spacing: 0.14px; + text-shadow: inherit; + + &:active { + background-color: transparent; + } + + .dropdown-arrow { + flex: none; + } +} + #project-dropdown-toggle { display: flex; /* adjust margins to keep position #project-title position stable after extension is loaded */ diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index ebebee4904..8a397a00db 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -166,6 +166,7 @@ define(function (require, exports, module) { require("spec/NpmNodeShim-test"); require("spec/AIImageTools-test"); require("spec/AICliConnector-test"); + require("spec/AIModelEffort-test"); // pro test suite optional components require("./pro-test-suite"); // todo TEST_MODERN diff --git a/test/spec/AIModelEffort-test.js b/test/spec/AIModelEffort-test.js new file mode 100644 index 0000000000..e88912ab9e --- /dev/null +++ b/test/spec/AIModelEffort-test.js @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2021 - present core.ai + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/*global describe, it, expect, beforeAll, awaitsFor */ + +define(function (require, exports, module) { + const NodeConnector = require("NodeConnector"); + + // The check runs in PhNode; browser jobs have no Node runtime. + if (!Phoenix.isNativeApp) { + return; + } + + // SDK supportedModels() rows, as the AI panel's queries see them. + const MODELS = [ + { value: "default", resolvedModel: "claude-fable-5-1", supportsEffort: true, + supportedEffortLevels: ["low", "medium", "high", "xhigh", "max"] }, + { value: "opus", resolvedModel: "claude-opus-5-5", supportsEffort: true, + supportedEffortLevels: ["low", "medium", "high", "xhigh", "max"] }, + { value: "sonnet", resolvedModel: "claude-sonnet-5", supportsEffort: true, + supportedEffortLevels: ["low", "medium", "high"] }, + { value: "haiku", resolvedModel: "claude-haiku-4-5", supportsEffort: false } + ]; + + describe("unit:AI Model Effort", function () { + let nodeConnector; + + beforeAll(async function () { + await awaitsFor(NodeConnector.isNodeReady, "Node runtime to be ready"); + nodeConnector = NodeConnector.createNodeConnector("ph_test_ai_model_effort", exports); + }); + + async function effortFor(request) { + return (await nodeConnector.execPeer("effortForQuery", request)).effort; + } + + it("sends no effort when none is asked for, so the system default applies", async function () { + expect(await effortFor({ model: "opus", models: MODELS })).toBe(null); + expect(await effortFor({ models: MODELS, resolvedDefaultModel: "claude-opus-5-5" })).toBe(null); + }); + + it("sends a level the model reports, matched by alias or resolved id", async function () { + expect(await effortFor({ effort: "max", model: "opus", models: MODELS })).toBe("max"); + expect(await effortFor({ effort: "low", model: "claude-sonnet-5", models: MODELS })).toBe("low"); + }); + + it("drops a level the model does not report, or any level for a model without effort", async function () { + expect(await effortFor({ effort: "xhigh", model: "sonnet", models: MODELS })).toBe(null); + expect(await effortFor({ effort: "low", model: "haiku", models: MODELS })).toBe(null); + expect(await effortFor({ effort: "ultra", model: "opus", models: MODELS })).toBe(null); + }); + + it("drops any level for a model the list does not describe", async function () { + expect(await effortFor({ effort: "high", model: "custom-model-7", models: MODELS })).toBe(null); + }); + + it("checks the default model against the model it resolved to, not the list's default row", async function () { + expect(await effortFor({ effort: "max", models: MODELS, + resolvedDefaultModel: "claude-sonnet-5" })).toBe(null); + expect(await effortFor({ effort: "high", models: MODELS, + resolvedDefaultModel: "claude-sonnet-5" })).toBe("high"); + expect(await effortFor({ effort: "high", models: MODELS, + resolvedDefaultModel: "claude-unlisted-9" })).toBe(null); + }); + + it("keeps a level the panel checked when this process has no model list yet", async function () { + expect(await effortFor({ effort: "high", model: "opus" })).toBe("high"); + expect(await effortFor({ effort: "high", models: MODELS })).toBe("high"); + }); + + it("never sends Anthropic effort levels to a custom endpoint", async function () { + expect(await effortFor({ effort: "high", model: "opus", models: MODELS, + customEndpoint: true })).toBe(null); + expect(await effortFor({ effort: "high", model: "opus", customEndpoint: true })).toBe(null); + }); + }); +}); diff --git a/tracking-repos.json b/tracking-repos.json index 842a9e8c43..19f65a113a 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "e5f5c116c344b8316bd127f572171119b17d2158" + "commitID": "18799f3e83dfc7f8d4e351e15d367bb053587081" } }