diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index da39742542..f3f5394037 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -2139,8 +2139,14 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // error. Custom API-key providers are excluded — their fix is the // settings-dialog hint appended above. const usingApiKey = !!(envOverrides && envOverrides.ANTHROPIC_AUTH_TOKEN); + // `\b401\b` is the load-bearing match: error phrasing keeps changing + // across CLI/SDK versions ("Failed to authenticate", "OAuth access + // token has expired", "token revoked"...) but the status code stays + // 401. Phrase alternatives remain for exit-code failures where the + // CLI prints a /login hint without any status code. 403 is + // deliberately excluded — it means "forbidden", not "re-login". const isAuthError = !usingApiKey && - /run \/login|invalid api key|not logged in|oauth token|revoke|authentication[_ ]?error/i + /run \/login|invalid api key|not logged in|oauth[\w ]*token|\b401\b|re-?authenticate|revoke|authentication[_ ]?error/i .test(detailedError); nodeConnector.triggerPeer("aiError", { diff --git a/src-node/lsp-client.js b/src-node/lsp-client.js index d01506a070..16307cd6dd 100644 --- a/src-node/lsp-client.js +++ b/src-node/lsp-client.js @@ -424,7 +424,15 @@ exports.startServer = async function startServer(params) { }); serverProcess.on('exit', (code, signal) => { - servers.delete(serverId); + // Only clear the registry when this process is still the registered generation. + // During a fast restart the old process's exit event can land after the + // replacement was already registered - an unconditional delete would remove the + // NEW server's entry, so its initialize response gets dropped in handleMessage + // (servers.get finds nothing) and every later request fails "not running" while + // the replacement process leaks, still alive but unreachable. + if (servers.get(serverId) === serverState) { + servers.delete(serverId); + } const stderr = serverState.stderrTail.join(''); if (code) { console.error(`[lsp-client][${serverId}] exited code=${code} signal=${signal || 'none'}`); @@ -435,7 +443,7 @@ exports.startServer = async function startServer(params) { rejectPending(new Error(`Server ${serverId} exited with pending request`)); } serverState.pending.clear(); - nodeConnector.triggerPeer('serverExit', { serverId, code, signal, stderr }); + nodeConnector.triggerPeer('serverExit', { serverId, code, signal, stderr, pid: serverProcess.pid }); if (!hasResolved) { hasResolved = true; reject(new Error(`Server ${serverId} exited immediately with code ${code}` + @@ -445,7 +453,9 @@ exports.startServer = async function startServer(params) { serverProcess.on('error', (err) => { console.error(`[lsp-client][${serverId}] spawn error:`, err.message); - servers.delete(serverId); + if (servers.get(serverId) === serverState) { + servers.delete(serverId); + } nodeConnector.triggerPeer('serverError', { serverId, error: err.message }); if (!hasResolved) { hasResolved = true; diff --git a/src/JSUtils/ScopeManager.js b/src/JSUtils/ScopeManager.js index b74267e946..ab6944cac1 100644 --- a/src/JSUtils/ScopeManager.js +++ b/src/JSUtils/ScopeManager.js @@ -355,6 +355,9 @@ define(function (require, exports, module) { * @return {string} the text, or the empty text if the original was too long */ function filterText(text) { + // Callers outside this module (e.g. JavaScriptRefactoring's highlight-references) can hit + // this before any Tern init/projectOpen has populated `preferences` - init it on demand. + ensurePreferences(); var newText = text; if (text.length > preferences.getMaxFileSize()) { newText = ""; diff --git a/src/languageTools/LSPClient.js b/src/languageTools/LSPClient.js index f9646cee99..d8141183e8 100644 --- a/src/languageTools/LSPClient.js +++ b/src/languageTools/LSPClient.js @@ -131,7 +131,14 @@ define(function (require, exports, module) { /** Convert a server `file://` URI (real OS path) back to a VFS-based `file://` URI. */ function serverUriToVfsUri(serverUri) { - const platformPath = PathConverters.uriToPath(serverUri); + let platformPath = PathConverters.uriToPath(serverUri); + // Windows language servers (e.g. tsserver) lowercase the drive letter in URIs + // ("file:///c%3A/..."), but Phoenix VFS mounts use the OS-reported uppercase drive + // ("/tauri/C/..."). The VFS is case-sensitive, so an un-normalized drive letter maps the + // same file to a second path - jump-to-definition would open a duplicate document. + if (brackets.platform === "win" && /^[a-z]:/.test(platformPath)) { + platformPath = platformPath.charAt(0).toUpperCase() + platformPath.substr(1); + } return PathConverters.pathToUri(_toVirtualPath(platformPath)); } @@ -239,12 +246,19 @@ define(function (require, exports, module) { if (!client) { return; } + if (data.pid && client._pid && data.pid !== client._pid) { + // Stale exit from a previous process generation - a restart has already spawned the + // replacement, so this must not clear its state or read as a crash of the new process. + return; + } client.capabilities = null; DocumentSync.clearServer(client); - if (client._stopping || _isDisabledByPref(client.serverId)) { - // Intentional stop/restart - do not auto-restart here. The pref check also covers - // the pref-off stop: its exit event can land after stopServerProcess resolved (and - // reset _stopping), which would otherwise read as a crash and bump _crashCount. + if (client._stopping || client._restarting || _isDisabledByPref(client.serverId)) { + // Intentional stop/restart - do not auto-restart here. The _restarting/pref checks + // also cover the stop's exit event landing after stopServerProcess resolved (and + // reset _stopping), which would otherwise read as a crash, bump _crashCount, and + // schedule an auto-restart that races the restart already in flight (two concurrent + // starts orphan one initialize request, which then times out). return; } // Unexpected crash - log it loudly (with the server's stderr) so failures are never @@ -799,7 +813,7 @@ define(function (require, exports, module) { client.rootUri = rootUri; client.rootName = rootName; - await conn.execPeer("startServer", { + const startResult = await conn.execPeer("startServer", { serverId: client.serverId, command: config.command, args: config.args || ["--stdio"], @@ -807,6 +821,9 @@ define(function (require, exports, module) { workspaceConfiguration: config.workspaceConfiguration, suppressStderrPattern: config.suppressStderrPattern }); + // Process-generation marker: lets _onServerExit tell a stale exit event (a previous + // process, delivered after its replacement already spawned) from a crash of this one. + client._pid = (startResult && startResult.pid) || null; const initResult = await conn.execPeer("sendRequest", { serverId: client.serverId, @@ -1118,8 +1135,11 @@ define(function (require, exports, module) { if (!client || _isDisabledByPref(serverId)) { return; } - await stopServerProcess(client); + // Held for the whole stop+start so the stopped process's exit event - which can land any + // time in between - is never mistaken for a crash (see _onServerExit). + client._restarting = true; try { + await stopServerProcess(client); await _startAndInit(client); _announceServerStarted(client); DocumentSync.openSupportedDocuments(client); @@ -1134,6 +1154,8 @@ define(function (require, exports, module) { Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "RstErr." + client._metricLabel); window.logger.reportErrorOnce("lspStart." + serverId, err, "[LSP] restart failed: " + serverId); + } finally { + client._restarting = false; } } diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 255989b398..e30af57fc3 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2664,6 +2664,11 @@ define({ "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_DEFAULT_DESC": "Recommended — uses your Claude Code model setting", + "AI_CHAT_MODEL_DESC_FABLE": "Most intelligent model — only if your plan has Fable access", + "AI_CHAT_MODEL_DESC_OPUS": "Powerful model for complex tasks", + "AI_CHAT_MODEL_DESC_SONNET": "Balanced speed and capability for everyday coding", + "AI_CHAT_MODEL_DESC_HAIKU": "Fastest model for quick, simple tasks", "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", diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 9f9a60b125..45ad1a1a30 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -160,7 +160,7 @@ define(function (require, exports, module) { const EVENT_PROJECT_CHANGED_OR_RENAMED_PATH = "projectChangedPath"; - EventDispatcher.setLeakThresholdForEvent(EVENT_PROJECT_OPEN, 25); + EventDispatcher.setLeakThresholdForEvent(EVENT_PROJECT_OPEN, 30); const CLIPBOARD_SYNC_KEY = "phoenix.clipboard"; diff --git a/tracking-repos.json b/tracking-repos.json index f58c08b100..c20e82a590 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "4aa07ee038916c8765baa9f87959759a657b9221" + "commitID": "e949adfa964caa7ba6e294be475124452b97110c" } }