From ad7850eb33e581ac80b54e01dc51b0b8431971e5 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Oct 2026 12:46:50 +0530 Subject: [PATCH 1/8] feat(ai): add CLI session controls and shared dropdown styling Add localized labels for adding, identifying, renaming, stopping and removing CLI sessions, including the running-session removal confirmation. Style the session action row, separate ellipsis control and inline name editor while preserving the original home cards and expanding grid. Reuse the Files project dropdown's quiet trigger styling for Visual AI and Add CLI session. Share its typography, spacing, arrow and interaction states, removing conflicting generic button and custom hover styles. Validation: full repository lint, LESS compilation and whitespace checks passed. Live DOM comparisons verified matching rest, hover and pressed styles; the card workflow and default provider bylines were reviewed live. The final byline change's Linux suite rerun was interrupted by a native Electron crash; earlier platform results are recorded in the Pro handoff. Companion Pro implementation: 5813290b25c65e4f631211e9cc7e26ebe9254d08. Pro dependency pins remain unchanged. --- src/nls/root/strings.js | 9 ++++ src/styles/Extn-AIChatPanel.less | 67 ++++++++++++++++++++--------- src/styles/Extn-RecentProjects.less | 25 +++++++++++ 3 files changed, 81 insertions(+), 20 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index ce8a94640f..7e929e0711 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!", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 26057df577..223de55a40 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; 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 */ From a6a0a618faf65c4079ff06f097e43a7c2ad010a2 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Oct 2026 12:55:04 +0530 Subject: [PATCH 2/8] build: update Pro dependency pin for multiple CLI sessions Advance the shared Phoenix Pro pin from e5f5c116 to 5813290b so builds use the multiple-session workflow paired with the core session controls. The pinned revision includes inline rename, state-aware Stop and Remove actions, provider bylines and session-specific terminal and draft routing. Validation: tracking-repos.json parses and its pin exactly matches the local Pro HEAD. The build's clone and checkout verification both consume this single pin. No application code or other dependency pins changed. --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index 842a9e8c43..8045c00205 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "e5f5c116c344b8316bd127f572171119b17d2158" + "commitID": "5813290b25c65e4f631211e9cc7e26ebe9254d08" } } From ba8052bef97cad913726b3a86bfa77f409e76f7b Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Oct 2026 13:15:39 +0530 Subject: [PATCH 3/8] build: update Pro pin for Ask AI clipboard and visibility fixes Advance the Phoenix Pro dependency from 5813290b to 3a5d115a so builds include image paste in the floating Ask AI dialog and honor View > Enable AI in the HTML element toolbar and its context menu. The pinned revision also records the deferred click-to-focus request for both AI composers. Validation: the Pro revision passed 87 registered native Jasmine checks each on Windows and macOS; Linux image paste was verified by the user. Confirm tracking-repos.json parses and its pin matches local Pro HEAD. The existing build checkout and verification paths both consume this shared dependency pin; no other dependencies changed. --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index 8045c00205..b120d8a3fc 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "5813290b25c65e4f631211e9cc7e26ebe9254d08" + "commitID": "3a5d115adee59b6b975b7bca3c297a3363eb8052" } } From ed675c60147ceabcedc5065da4107b1f0cd9ea0b Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Oct 2026 15:51:07 +0530 Subject: [PATCH 4/8] feat(ai): send model-aware thinking effort from the AI panel Pass the AI panel's chosen thinking effort to Claude Code queries only when the model supports that level. A new src-node helper re-checks the level against the SDK model list (or the resolved default on the same endpoint), drops it for unknown models and any custom endpoint, and leaves effort unset for the default so the system default applies. Add the effort strings and the chip and dropdown styles, and hide the usage chip while the Enter-to-send hint flashes so the hint fits. Tests: unit:AI Model Effort (7) through a dedicated PhNode test connector. --- src-node/ai-model-effort.js | 64 +++++++++ src-node/claude-code-agent.js | 35 ++++- src-node/test-connection.js | 1 + src-node/test/test-ai-model-effort.js | 35 +++++ src/nls/root/strings.js | 15 +++ src/styles/Extn-AIChatPanel.less | 179 +++++++++++++++++++++++++- test/UnitTestSuite.js | 1 + test/spec/AIModelEffort-test.js | 79 ++++++++++++ 8 files changed, 400 insertions(+), 9 deletions(-) create mode 100644 src-node/ai-model-effort.js create mode 100644 src-node/test/test-ai-model-effort.js create mode 100644 test/spec/AIModelEffort-test.js 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 7e929e0711..d101f27422 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -3001,6 +3001,21 @@ 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_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 223de55a40..ed726fe63d 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -4526,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; @@ -7017,3 +7012,177 @@ 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)"; + } + + .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%; + margin: 0; + accent-color: @ai-effort-accent; + cursor: pointer; + + &.ai-effort-dim { + opacity: 0.35; + } + } + + // 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; + transform: translateX(-50%); + white-space: nowrap; + } + + .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/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); + }); + }); +}); From 31429841ea528180d0976624a428678427bd1582 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Oct 2026 15:51:40 +0530 Subject: [PATCH 5/8] build: update Pro pin for model-aware thinking effort Advance the Phoenix Pro dependency from 3a5d115a to 7594bacf so builds include the AI panel's thinking effort chip and dropdown, paired with the core effort validation and styles. Validation: tracking-repos.json parses and its pin matches local Pro HEAD. The effort suites passed on Windows, macOS and Linux (full unit category plus effort, CLI composer and Live Preview send integration suites); the final dropdown version passed 10/10 on Linux. No other dependencies changed. --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index b120d8a3fc..b10aac5b37 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "3a5d115adee59b6b975b7bca3c297a3363eb8052" + "commitID": "7594bacf4c1e1408f661f79dddfd6744cc2abfcf" } } From 9731dfe8f23148fdef20513d21d974dfd7cd5afd Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Oct 2026 16:37:57 +0530 Subject: [PATCH 6/8] fix(ai): clarify effort bulb and slider states Use muted gold only for Extra high and Max bulbs, with the approved softened glow reserved for Max. Provide light and dark theme colors without changing labels or lower effort levels. Render Default as a neutral empty slider track with no thumb or filled range, so the previous selected position cannot be mistaken for the system default. Keep the track interactive and style selectable level labels like the existing markers. Add a localized tooltip for the current effort. Advance the Pro pin to b4bf851d to include the matching compact chip, accessible state and click-selection behavior with its registered UI regression coverage. Validation: all 15 AI Thinking Effort integration specs pass on Windows, macOS and Linux. Light/dark colors, Max-to-Default reset and click selection are covered. LESS compiles, targeted ESLint has no errors or new warnings, and tracking-repos.json parses with its Pro pin matching the verified commit. --- src/nls/root/strings.js | 1 + src/styles/Extn-AIChatPanel.less | 62 +++++++++++++++++++++++++++++++- tracking-repos.json | 2 +- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index d101f27422..c6a7c804b1 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -3002,6 +3002,7 @@ define({ "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", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index ed726fe63d..a65a153fac 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -7054,6 +7054,23 @@ 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; @@ -7145,12 +7162,44 @@ .ai-effort-slider { display: block; width: 100%; + height: 16px; margin: 0; accent-color: @ai-effort-accent; cursor: pointer; &.ai-effort-dim { - opacity: 0.35; + // 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; + } } } @@ -7166,8 +7215,19 @@ .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 { diff --git a/tracking-repos.json b/tracking-repos.json index b10aac5b37..b0ec40f124 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "7594bacf4c1e1408f661f79dddfd6744cc2abfcf" + "commitID": "b4bf851d1f6bf83adce8581a974a7005f3b1ee58" } } From 0464583dc33ec89024db8030ad2986318f191a24 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Oct 2026 17:13:23 +0530 Subject: [PATCH 7/8] build: update Pro pin for CI test fixes Pin Phoenix Pro to cb3f10626fcdf7a734aecccac519a79c4b42194f, including the visible unsupported-environment marker for CLI Sessions, awaited PTY fixture cleanup, and the recorded inbox-monitor verification. The marker prevents empty-suite registration from aborting desktop Linux and browser test startup. Waiting for terminal kills avoids handing the next suite a fixture directory still held open by a Windows shell. Validation: CLI suites 52/52 on Windows, macOS and Linux; rebuilt Linux CI unit suite 3045/3045 on rerun; Chromium unit suite 2859/2859. Browser integration had one timeout, cleared by its complete 7/7 suite rerun. The Pro handoff records broader-run limitations and the hosted Actions follow-up after the user's push. --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index b0ec40f124..732cd7703d 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "b4bf851d1f6bf83adce8581a974a7005f3b1ee58" + "commitID": "cb3f10626fcdf7a734aecccac519a79c4b42194f" } } From 00ec0899128ff115ae8424339dfea76ceb34f782 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Oct 2026 19:51:55 +0530 Subject: [PATCH 8/8] build: update pro deps --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index 732cd7703d..19f65a113a 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "cb3f10626fcdf7a734aecccac519a79c4b42194f" + "commitID": "18799f3e83dfc7f8d4e351e15d367bb053587081" } }