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
8 changes: 7 additions & 1 deletion src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down
16 changes: 13 additions & 3 deletions src-node/lsp-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'}`);
Expand All @@ -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}` +
Expand All @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/JSUtils/ScopeManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "";
Expand Down
36 changes: 29 additions & 7 deletions src/languageTools/LSPClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,14 @@

/** 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));
}

Expand Down Expand Up @@ -239,12 +246,19 @@
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
Expand Down Expand Up @@ -799,14 +813,17 @@
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"],
rootUri: rootUri,
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;

Check warning on line 826 in src/languageTools/LSPClient.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ_Coq-4-CkDYWncg7X0&open=AZ_Coq-4-CkDYWncg7X0&pullRequest=3065

const initResult = await conn.execPeer("sendRequest", {
serverId: client.serverId,
Expand Down Expand Up @@ -1118,8 +1135,11 @@
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);
Expand All @@ -1134,6 +1154,8 @@
Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "RstErr." + client._metricLabel);
window.logger.reportErrorOnce("lspStart." + serverId, err,
"[LSP] restart failed: " + serverId);
} finally {
client._restarting = false;
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/project/ProjectManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

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": "4aa07ee038916c8765baa9f87959759a657b9221"
"commitID": "e949adfa964caa7ba6e294be475124452b97110c"
}
}
Loading