Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 55 additions & 1 deletion src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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_
Expand Down Expand Up @@ -2090,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
Expand Down
8 changes: 8 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -2647,6 +2648,13 @@ 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",
"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",
Expand Down
104 changes: 103 additions & 1 deletion src/styles/Extn-AIChatPanel.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -162,12 +192,84 @@
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;
}
}

/* 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. */
.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 ──────────────────────────────────────── */
Expand Down
2 changes: 1 addition & 1 deletion tracking-repos.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"phoenixPro": {
"commitID": "5b52e051868bfb97e167e3c0233733443cd68701"
"commitID": "a12762f514f0bb13b002de5b55e5be76d6e7c8cb"
}
}
Loading