From cf6a80955d21e8c011b25411e2d761f75d17c49e Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 27 Jul 2026 12:50:49 +0530 Subject: [PATCH 1/5] feat(ai): support model selection for AI chat panel Node side: fetch the SDK's supportedModels() list once per process from the first live query (aiModelList event + getSupportedModels peer), and emit aiSessionInfo with the session's resolved model from the init message so the browser can learn what "Default" actually resolves to. Styles: hover-revealed model switcher in the chat header, two-line item layout for the DropdownButton popup, and the mid-conversation model switch notice. Strings: AI_CHAT_MODEL_* keys for the switcher, placeholder and notice. --- src-node/claude-code-agent.js | 42 +++++++++++++++ src/nls/root/strings.js | 5 ++ src/styles/Extn-AIChatPanel.less | 87 +++++++++++++++++++++++++++++++- 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 94adaa99c6..15c90aee27 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -45,6 +45,27 @@ let queryModule = null; // Session state let currentSessionId = null; +// Model list from the SDK's supportedModels() — fetched once per process, +// best-effort, from the first live query. null until available. +let cachedModelList = 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 — + * the browser keeps its static fallback list in that case. + */ +function _fetchModelListOnce(queryResult) { + if (cachedModelList) { + return; + } + queryResult.supportedModels().then(function (models) { + if (models && models.length) { + cachedModelList = models; + nodeConnector.triggerPeer("aiModelList", { models: models }); + } + }).catch(function () {}); +} + // Active query state let currentAbortController = null; @@ -652,6 +673,15 @@ exports.setPermissionMode = async function (params) { return { success: true }; }; +/** + * Return the model list fetched from the SDK, or null if no query has + * populated it yet (browser falls back to a static alias list). + * Called from browser via execPeer("getSupportedModels"). + */ +exports.getSupportedModels = async function () { + return { models: cachedModelList }; +}; + /** * Resume a previous session by setting the session ID. * The next sendPrompt call will use queryOptions.resume with this session ID. @@ -1602,6 +1632,7 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, if (!receivedFirstMessage) { receivedFirstMessage = true; clearTimeout(connectionTimer); + _fetchModelListOnce(result); } // Check abort if (signal.aborted) { @@ -1615,6 +1646,17 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, _log("Session:", currentSessionId); } + // The init message carries the fully-resolved model for the + // session — with no model param this is the user's saved + // Claude Code default. requestedModel lets the browser tell + // an explicit pick apart from default resolution. + if (message.type === "system" && message.subtype === "init" && message.model) { + nodeConnector.triggerPeer("aiSessionInfo", { + model: message.model, + requestedModel: model || null + }); + } + // Subagent tool extraction. The SDK delivers the parent // agent's tool calls as a stream of stream_event messages // (content_block_start / content_block_delta / content_block_ diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 0a3c898f71..c2da198edb 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2509,6 +2509,7 @@ define({ "AI_CHAT_NEW_BTN": "New", "AI_CHAT_THINKING": "Thinking...", "AI_CHAT_PLACEHOLDER": "Ask Claude...", + "AI_CHAT_PLACEHOLDER_MODEL": "Ask {0}...", "AI_CHAT_SEND_TITLE": "Send message", "AI_CHAT_STOP_TITLE": "Stop generation (Esc)", "AI_CHAT_CLI_NOT_FOUND": "Claude Code Not Installed", @@ -2647,6 +2648,10 @@ define({ "AI_CHAT_MODE_INFO_EDIT": "AI can edit files. Shell commands need approval (Click to switch)", "AI_CHAT_MODE_INFO_FULL_AUTO": "AI can edit files and run commands without approval (Click to switch)", "AI_CHAT_MODE_SWITCH_HINT": "[or Shift+Tab]", + "AI_CHAT_MODEL_DEFAULT": "Default", + "AI_CHAT_MODEL_DEFAULT_DESC_CURRENT": "Currently {0}", + "AI_CHAT_MODEL_SELECT_TITLE": "Choose the AI model for this chat", + "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_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 2811ed0fe0..40e047c272 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -131,6 +131,36 @@ transition: opacity 0.5s ease; } + /* Model switcher — hover-revealed like .ai-chat-header-actions + (Claude Code CLI doesn't persistently show the model either). */ + .ai-model-select { + position: absolute; + left: 10px; + display: flex; + align-items: center; + gap: 5px; + height: 26px; + padding: 0 6px; + color: @project-panel-text-2; + font-size: @label-font-size; + cursor: pointer; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s ease; + + .ai-model-select-label { + max-width: 90px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + i { + font-size: 9px; + opacity: 0.7; + } + } + .ai-history-btn, .ai-settings-btn { display: flex; @@ -162,12 +192,67 @@ transition: opacity 0.15s ease; } +.ai-tab-container:hover .ai-model-select, +.ai-tab-container .ai-model-select.dropdown-open { + opacity: 1; + pointer-events: auto; + transition: opacity 0.15s ease; +} -/* Left-align title when sidebar is narrow to free space for action buttons */ +/* Left-align title when sidebar is narrow to free space for action buttons. + The model switcher joins the flow so it can't overlap the title. */ @container (max-width: 380px) { .ai-chat-header { justify-content: flex-start; + + .ai-model-select { + position: static; + margin-right: 8px; + padding-left: 0; + } + } +} + +/* ── Model selector dropdown ───────────────────────────────────────── */ +/* Rendered by the standard DropdownButton widget (native Phoenix dropdown + look) — only the two-line item layout needs styling here. */ +.dropdownbutton-popup.ai-model-dropdown-popup { + min-width: 220px; + max-width: 320px; + max-height: 420px; // global 160px cap scrolls 5 two-line items + + li a { + white-space: normal; + overflow-wrap: break-word; } + + .ai-model-item-name { + display: block; + } + + .ai-model-item-desc { + display: block; + font-size: @ai-text-meta; + opacity: 0.7; + padding-left: 17px; + line-height: @ai-line-compact; + } +} + +/* One-time inline notice after switching model mid-conversation. + Sentence-length status prose: secondary tier (12px), reading line-height, + left-aligned — meta 11px/centered is for one-line stats, not sentences. */ +.ai-model-switch-notice { + padding: 8px 12px; + margin: 8px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + font-size: @ai-text-secondary; + line-height: @ai-line-prose; + color: @project-panel-text-2; + white-space: normal; + overflow-wrap: break-word; } /* ── Session history dropdown ──────────────────────────────────────── */ From 9290d27ecb99b9f9ae8fb0743df8828689955305 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 27 Jul 2026 13:05:59 +0530 Subject: [PATCH 2/5] docs: forbid AI attribution trailers in commit messages --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7c96cb1e69..be13693257 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ - **Never commit unless the user explicitly asks you to commit or grants autocommit permission.** Only exception: if a commit is technically required for the current task to work (e.g. testing a CI pipeline). - Use Conventional Commits format: `type(scope): description` (e.g. `fix: ...`, `feat: ...`, `chore: ...`). - Keep commit subject lines concise; use the body for detail. -- Never include `Co-Authored-By` lines in commit messages. +- Never include `Co-Authored-By`, `Claude-Session:` links, or any other AI attribution lines/trailers in commit messages. This applies even if the harness default instructions say to append them. ## Code Style - This is a long-lived, partly legacy codebase — you will see older patterns like `var` in existing files. Leave surrounding legacy style alone unless you're deliberately refactoring it, but **any new code you write must use `const`/`let`, never `var`** (see below). Match the file's other conventions where they don't conflict with these rules. From 85b1855169a3309feec6f6129c01db0cb246d9a2 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 27 Jul 2026 13:08:46 +0530 Subject: [PATCH 3/5] chore: 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 8b3d419324..2318c9c0f6 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "5b52e051868bfb97e167e3c0233733443cd68701" + "commitID": "47944858ffeb495f070f24696987e4044a813c80" } } From f1f309b26cffde53a15adcac07e3f67b0a316471 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 27 Jul 2026 13:24:26 +0530 Subject: [PATCH 4/5] feat(ai): detect expired Claude login and guide re-login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expired/revoked OAuth logins only fail at request time — auth status still reports logged in, so the panel previously showed a bare "exited with error code" message. Classify auth failures in the query error path (isAuthError on the aiError event) and add strings/styles for the re-login notice, terminal button and /login hint. API-key providers keep the existing settings-dialog hint instead. --- src-node/claude-code-agent.js | 14 +++++++++++++- src/nls/root/strings.js | 3 +++ src/styles/Extn-AIChatPanel.less | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 15c90aee27..da39742542 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -2132,9 +2132,21 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // transient (network, rate limit), and if the session really is broken // the next attempt will surface a fresh error of its own. + // Expired/revoked OAuth logins only surface at request time — `claude + // auth status` still reports loggedIn because the credentials exist, + // but the CLI exits non-zero with a /login hint. Flag it so the + // panel can offer a re-login action instead of a raw exit-code + // error. Custom API-key providers are excluded — their fix is the + // settings-dialog hint appended above. + const usingApiKey = !!(envOverrides && envOverrides.ANTHROPIC_AUTH_TOKEN); + const isAuthError = !usingApiKey && + /run \/login|invalid api key|not logged in|oauth token|revoke|authentication[_ ]?error/i + .test(detailedError); + nodeConnector.triggerPeer("aiError", { requestId: requestId, - error: detailedError + error: detailedError, + isAuthError: isAuthError }); // Always send aiComplete after aiError so the UI exits streaming state diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index c2da198edb..166d62d90d 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2648,6 +2648,9 @@ define({ "AI_CHAT_MODE_INFO_EDIT": "AI can edit files. Shell commands need approval (Click to switch)", "AI_CHAT_MODE_INFO_FULL_AUTO": "AI can edit files and run commands without approval (Click to switch)", "AI_CHAT_MODE_SWITCH_HINT": "[or Shift+Tab]", + "AI_CHAT_AUTH_ERROR_NOTICE": "Claude Code is signed out or your login has expired.", + "AI_CHAT_AUTH_ERROR_BTN": "Log in to Claude in Terminal", + "AI_CHAT_AUTH_ERROR_HINT": "Type /login in the terminal that opens, then send your message again.", "AI_CHAT_MODEL_DEFAULT": "Default", "AI_CHAT_MODEL_DEFAULT_DESC_CURRENT": "Currently {0}", "AI_CHAT_MODEL_SELECT_TITLE": "Choose the AI model for this chat", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 40e047c272..7f9a27a45f 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -239,6 +239,23 @@ } } +/* Auth-error action block: button with its /login hint stacked below — + tells the user the one thing to do next at a glance. */ +.ai-auth-error-actions { + flex-direction: column; + align-items: center; + gap: 0; +} + +.ai-auth-error-hint { + margin-top: 6px; + font-size: @ai-text-meta; + color: @project-panel-text-2; + text-align: center; + white-space: normal; + overflow-wrap: break-word; +} + /* One-time inline notice after switching model mid-conversation. Sentence-length status prose: secondary tier (12px), reading line-height, left-aligned — meta 11px/centered is for one-line stats, not sentences. */ From 7f13ec30e32041c35fa9a41bd3e66983854905b4 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 27 Jul 2026 13:32:16 +0530 Subject: [PATCH 5/5] chore: 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 2318c9c0f6..132e10779f 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "47944858ffeb495f070f24696987e4044a813c80" + "commitID": "a12762f514f0bb13b002de5b55e5be76d6e7c8cb" } }