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
11 changes: 9 additions & 2 deletions src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2499,8 +2499,8 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
// Per-turn token usage: each SDKAssistantMessage carries the
// wrapped Anthropic API message whose `.usage` reflects what
// that single turn consumed. Useful for diagnosing runaway
// loops; logged but not metric'd individually (the result
// message rolls up the session totals).
// loops. The panel keeps these in RAM until the result replaces
// them; message ids let it dedupe repeated content blocks.
if (message.type === "assistant" &&
message.message && message.message.usage) {
const u = message.message.usage;
Expand All @@ -2510,6 +2510,13 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
"cacheRead=" + (u.cache_read_input_tokens || 0),
"cacheCreate=" + (u.cache_creation_input_tokens || 0),
message.parent_tool_use_id ? "(subagent)" : "");
nodeConnector.triggerPeer("aiUsageProgress", {
requestId: requestId,
messageId: message.message.id,
parentToolUseId: message.parent_tool_use_id || null,
model: message.message.model,
usage: u
});
}

// Aggregate session usage on the terminal `result` message.
Expand Down
95 changes: 71 additions & 24 deletions src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,43 @@
// send(msgStr) - sends the given message string over the transport.
var transport = global._Brackets_LiveDev_Transport;

// The page's own scripts run after this file and may patch built-ins to read or rewrite what goes
// to and from the editor, so everything on that path uses the originals, captured here. The
// editor's Runtime.evaluate scripts run through the real eval, called indirectly so they see only
// the global scope, as they would from the page: never this closure's transport, comm or the CSS
// domain object that shadows window.CSS here.
const _globalEval = global.eval;
const _parse = JSON.parse;
const _stringify = JSON.stringify;
const _Promise = global.Promise;
const _setTimeout = global.setTimeout;
const _clearTimeout = global.clearTimeout;

/**
* Append without Array.prototype.push, which a page could patch to collect the item.
* @param {Array} arr Destination array.
* @param {*} item Item to append.
*/
function _arrayPush(arr, item) {
arr[arr.length] = item;
}

/**
* Copy nonmatching items without exposing the array to a page-patched filter.
* @param {Array} arr Source array.
* @param {function(*): boolean} predicate Returns true for items to remove.
* @return {Array} The remaining items, in order.
*/
function _arrayWithout(arr, predicate) {
const kept = [];
for (let i = 0; i < arr.length; i++) {
if (!predicate(arr[i])) {
_arrayPush(kept, arr[i]);
}
}

Check warning on line 71 in src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected a `for-of` loop instead of a `for` loop with this simple iteration.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaEARCqnM1Oq-5Seh6Xs&open=AaEARCqnM1Oq-5Seh6Xs&pullRequest=3255
return kept;
}

/**
* Manage messaging between Editor and Browser at the protocol layer.
* Handle messages that arrives through the current transport and dispatch them
Expand Down Expand Up @@ -68,16 +105,15 @@

if (msgHandlers && msgHandlers.length > 0) {
// invoke handlers with the received message
msgHandlers.forEach(function (handler) {
// A plain loop: a patched Array.prototype.forEach would be handed the message.
for (let i = 0; i < msgHandlers.length; i++) {
try {
// TODO: check which context should be used to call handlers here.
handler(msg);
return;
msgHandlers[i](msg);
} catch (e) {
console.error("[Brackets LiveDev] Error executing a handler for " + msg.method, e.stack);
return;
}
});
}

Check warning on line 116 in src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected a `for-of` loop instead of a `for` loop with this simple iteration.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaEARCqnM1Oq-5Seh6Xt&open=AaEARCqnM1Oq-5Seh6Xt&pullRequest=3255
} else {
// no subscribers, ignore it.
// TODO: any other default handling? (eg. specific respond, trigger as a generic event, etc.);
Expand Down Expand Up @@ -115,16 +151,15 @@
//initialize array
this.handlers[method] = [];
}
// add handler to the stack
this.handlers[method].push(handler);
_arrayPush(this.handlers[method], handler);
},

/**
* Send a message to the Editor.
* @param {string} msgStr Message to be sent.
*/
send: function (msgStr) {
transport.send(JSON.stringify(msgStr));
transport.send(_stringify(msgStr));
}
};

Expand All @@ -140,9 +175,9 @@
evaluate: function (msg) {
// an unanswered request leaves the editor side waiting forever
try {
var result = eval(msg.params.expression);
var result = _globalEval(msg.params.expression);

Check failure on line 178 in src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaEARCqnM1Oq-5Seh6Xu&open=AaEARCqnM1Oq-5Seh6Xu&pullRequest=3255
MessageBroker.respond(msg, {
result: JSON.stringify(result) // TODO: in original protocol this is an object handle
result: _stringify(result) // TODO: in original protocol this is an object handle

Check warning on line 180 in src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaEARCqnM1Oq-5Seh6Xv&open=AaEARCqnM1Oq-5Seh6Xv&pullRequest=3255
});
} catch (e) {
console.error("[Brackets LiveDev] Runtime.evaluate failed", e);
Expand Down Expand Up @@ -349,7 +384,7 @@
* @param {string} msgStr The protocol message as stringified JSON.
*/
message: function (msgStr) {
const msg = JSON.parse(msgStr);
const msg = _parse(msgStr);
_setPCommReady();
if(msg && typeof msg === "object" && msg.method === "PhoenixComm.execLPFn") {
_onLPFnTrigger(msg.fnName, msg.params);
Expand Down Expand Up @@ -397,7 +432,8 @@

let currentFnExecID = 1;
let lpCommReady = false;
const pendingExecPromises = new Map();
// A page-patched Map.prototype.set must not receive the promise's resolve/reject callbacks.
const pendingExecPromises = Object.create(null);
let queuedExecRequests = []; // array of { fnName, paramObj, fnExecID }
// A pending execPhoenixFn is only ever settled by a response message, so if
// the editor disconnects mid-call the promise would hang forever (and any
Expand All @@ -414,7 +450,7 @@
if (lpCommReady) {
MessageBroker.send(payload);
} else {
queuedExecRequests.push(payload);
_arrayPush(queuedExecRequests, payload);
}
}

Expand All @@ -437,21 +473,25 @@
}
registeredPhoenixCommFns[fnName] = fn;
},
/**
* Request an editor function, rejecting if no response arrives before the timeout.
* @param {string} fnName Registered editor function name.
* @param {*} paramObj Function parameters.
* @return {Promise<*>} The editor's response.
*/
execPhoenixFn: function (fnName, paramObj) {
return new Promise((resolve, reject) => {
return new _Promise((resolve, reject) => {
const fnExecID = currentFnExecID++;
const timer = setTimeout(function () {
if (pendingExecPromises.has(fnExecID)) {
pendingExecPromises.delete(fnExecID);
const timer = _setTimeout(function () {
if (pendingExecPromises[fnExecID]) {
delete pendingExecPromises[fnExecID];
// if still queued (comm never became ready), drop it so a
// late flush doesn't execute a call the caller saw fail
queuedExecRequests = queuedExecRequests.filter(function (req) {
return req.fnExecID !== fnExecID;
});
queuedExecRequests = _arrayWithout(queuedExecRequests, req => req.fnExecID === fnExecID);
reject(new Error(`execPhoenixFn timed out: ${fnName}`));
}
}, PHOENIX_FN_TIMEOUT_MS);
pendingExecPromises.set(fnExecID, { resolve, reject, timer });
pendingExecPromises[fnExecID] = { resolve, reject, timer };
_sendOrQueueExec({
execFnName: fnName,
paramObj,
Expand All @@ -468,17 +508,24 @@

PhoenixComm.registerLpFn("PH_LP_COMM_READY", _setPCommReady);

/**
* Settle a pending editor request and clear its timeout; ignore late or unknown replies.
* @param {string} fnName Editor function name, used for diagnostics.
* @param {number} fnExecID Request identifier.
* @param {*} resolveWith Successful result.
* @param {*} rejectWith Error returned by the editor, if any.
*/
function _onPhoenixExecResponse(fnName, fnExecID, resolveWith, rejectWith) {
const pendingPromise = pendingExecPromises.get(fnExecID);
const pendingPromise = pendingExecPromises[fnExecID];
if(!pendingPromise) {
// already settled (e.g. by the timeout) or an unknown id — bail
// rather than dereference undefined and throw in the message handler
console.error(`execPhoenixFn: No response promise found! for ${fnName}: ${fnExecID}`);
return;
}
pendingExecPromises.delete(fnExecID);
delete pendingExecPromises[fnExecID];
if (pendingPromise.timer) {
clearTimeout(pendingPromise.timer);
_clearTimeout(pendingPromise.timer);
}
if(rejectWith) {
pendingPromise.reject(rejectWith);
Expand Down
72 changes: 51 additions & 21 deletions src/LiveDevelopment/BrowserScripts/LivePreviewTransportRemote.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*
*/

/*global __PHOENIX_LIVE_PREVIEW_TRANSPORT_CONFIG__ */

// This is a transport injected into the browser via a script that handles the low
// level communication between the live development protocol handlers on both sides.
// The actual communication to phoenix is done via the loaded web worker below. We just post/receive all
Expand Down Expand Up @@ -89,20 +91,46 @@

(function (global) {

// The below line will be replaced with the transport scripts provided by the static server at
// LivePreviewTransport.js:getRemoteScript() This is so that the actual live preview page doesnt get hold of
// any phoenix web socket or broadcast channel ids from this closure programatically for security.

//Replace dynamic section start
const TRANSPORT_CONFIG={};
//Replace dynamic section end

// getRemoteScript() replaces this call with a private config initializer before serving the script.
// A call survives minification without depending on declaration spacing or merged const statements.
// Its unknown return value also prevents the minifier from folding config property reads.
const TRANSPORT_CONFIG = __PHOENIX_LIVE_PREVIEW_TRANSPORT_CONFIG__();

// The page's own scripts run after this one and may patch built-ins to read or rewrite what goes
// to and from the editor. The channel uses the originals, captured here before any of them runs.
const _apply = Reflect.apply;
const _workerPostMessage = Worker.prototype.postMessage;
const _messageEventData = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data").get;
const _stringify = JSON.stringify;
const _console = global.console;
const _consoleLog = _console.log;

/**
* Log only when enabled by the editor, using the original console method.
* @param {...*} args Values to log.
*/
function _debugLog(...args) {
if(window.LIVE_PREVIEW_DEBUG_ENABLED) {
console.log(...args);
if (TRANSPORT_CONFIG.LIVE_PREVIEW_DEBUG_ENABLED) {
_apply(_consoleLog, _console, args);
}
}

/**
* Send data through the captured Worker method.
* @param {Object} data Worker message.
*/
function _postToWorker(data) {
_apply(_workerPostMessage, worker, [data]);
}
/**
* Read worker data without invoking a page-patched getter.
* @param {MessageEvent} event Worker event.
* @return {*} The event payload.
*/
function _eventData(event) {
return _apply(_messageEventData, event, []);
}

function createLRU(max = 100) {
const map = new Map();

Expand Down Expand Up @@ -139,9 +167,10 @@
const worker = new Worker(TRANSPORT_CONFIG.LIVE_DEV_REMOTE_WORKER_SCRIPTS_FILE_NAME);
let _workerMessageProcessor;
worker.onmessage = (event) => {
const type = event.data.type;
const data = _eventData(event);
const type = data.type;
switch (type) {
case 'REDIRECT_PAGE': location.href = event.data.URL; break;
case 'REDIRECT_PAGE': location.href = data.URL; break;
default:
if(_workerMessageProcessor){
return _workerMessageProcessor(event);
Expand All @@ -151,15 +180,15 @@
};
// message channel to phoenix connect on load itself. The channel id is injected from phoenix
// via LivePreviewTransport.js while serving the instrumented html file
worker.postMessage({
_postToWorker({
type: "setupPhoenixComm",
livePreviewDebugModeEnabled: TRANSPORT_CONFIG.LIVE_PREVIEW_DEBUG_ENABLED,
broadcastChannel: TRANSPORT_CONFIG.LIVE_PREVIEW_BROADCAST_CHANNEL_ID, // in browser this will be present, but not in tauri
websocketChannelURL: TRANSPORT_CONFIG.LIVE_PREVIEW_WEBSOCKET_CHANNEL_URL, // in tauri this will be present. not in browser
clientID
});
function _postLivePreviewMessage(message) {
worker.postMessage({type: "livePreview", message});
_postToWorker({type: "livePreview", message});
}
let sentTitle, sentFavIconURL;

Expand Down Expand Up @@ -192,7 +221,7 @@
if(!base64){
base64 = "favicon.ico";
}
worker.postMessage({
_postToWorker({
type: "updateTitleIcon",
faviconBase64: base64
});
Expand All @@ -201,7 +230,7 @@

if(sentTitle!== document.title) {
sentTitle = document.title;
worker.postMessage({
_postToWorker({
type: "updateTitleIcon",
title: document.title
});
Expand Down Expand Up @@ -239,18 +268,19 @@

// Listen to the response
_workerMessageProcessor = (event) => {
const data = _eventData(event);
// Print the result
_debugLog("Live Preview: Browser received event from Phoenix: ", JSON.stringify(event.data));
const type = event.data.type;
_debugLog("Live Preview: Browser received event from Phoenix: ", _stringify(data));
const type = data.type;
switch (type) {
case 'BROWSER_CONNECT': break; // do nothing. This is a loopback message from another live preview tab
case 'BROWSER_MESSAGE': break; // do nothing. This is a loopback message from another live preview tab
case 'BROWSER_CLOSE': break; // do nothing. This is a loopback message from another live preview tab
case 'MESSAGE_FROM_PHOENIX':
if (self._callbacks && self._callbacks.message) {
const clientIDs = event.data.clientIDs,
message = event.data.message,
messageID = event.data.messageID;
const clientIDs = data.clientIDs,
message = data.message,
messageID = data.messageID;
if(messageID && processedMessageIDs.has(messageID)){
return; // we have already processed this message.
}
Expand Down
3 changes: 2 additions & 1 deletion src/LiveDevelopment/BrowserScripts/RemoteFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ function RemoteFunctions(config = {}) {
// this is just a test function here to check if live preview. fn call is working correctly.
console.log("Hello World", param);
});
const MessageBroker = window._Brackets_MessageBroker; // to be used by plugins.
// Injected plugins share this closure; the window property is removed after injection.
const MessageBroker = window._Brackets_MessageBroker;

const SHARED_STATE = {
__description: "Use this to keep shared state for Live Preview Edit instead of window.*",
Expand Down
1 change: 1 addition & 0 deletions src/LiveDevelopment/LivePreviewConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ define(function main(require, exports, module) {
exports.HIGHLIGHT_CLICK = "click";

exports.PREFERENCE_SHOW_RULER_LINES = "livePreviewShowMeasurements";
exports.PREFERENCE_SHOW_ASK_AI = "livePreviewShowAskAI";

exports.PREFERENCE_SHOW_STYLES_BAR = "livePreviewShowStylesBar";

Expand Down
Loading
Loading