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
33 changes: 33 additions & 0 deletions phoenix-builder-mcp/mcp-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,39 @@ export function registerTools(server, processManager, wsControlServer, phoenixDe
}
);

server.tool(
"exec_js_in_test_iframe",
"Execute JavaScript in the embedded test Phoenix iframe inside the SpecRunner, NOT in the SpecRunner itself. " +
"The iframe is usually not present during unit tests, but for other categories tests may spawn it as needed — " +
"it can come and go at any time. " +
"Code runs async in the iframe's page context with access to the test Phoenix instance's globals " +
"(jQuery $, brackets.test.*, etc.). " +
"Returns an error if no iframe is present. " +
"Use exec_js to control the SpecRunner (run tests, get results); use this tool to inspect the test Phoenix instance.",
{
code: z.string().describe("JavaScript code to execute in the test Phoenix iframe"),
instance: z.string().optional().describe("Target a specific test runner instance by name. Required when multiple instances are connected.")
},
async ({ code, instance }) => {
try {
const result = await wsControlServer.requestExecJsInTestIframe(code, instance);
return {
content: [{
type: "text",
text: result !== undefined ? String(result) : "(undefined)"
}]
};
} catch (err) {
return {
content: [{
type: "text",
text: JSON.stringify({ error: err.message })
}]
};
}
}
);

server.tool(
"run_tests",
"Run tests in the Phoenix test runner (SpecRunner.html). Reloads the test runner with the specified " +
Expand Down
49 changes: 49 additions & 0 deletions phoenix-builder-mcp/ws-control-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ export function createWSControlServer(port) {
break;
}

case "exec_js_in_test_iframe_response": {
const pending7 = pendingRequests.get(msg.id);
if (pending7) {
pendingRequests.delete(msg.id);
if (msg.error) {
pending7.reject(new Error(msg.error));
} else {
pending7.resolve(msg.result);
}
}
break;
}

case "run_tests_response": {
const pendingRt = pendingRequests.get(msg.id);
if (pendingRt) {
Expand Down Expand Up @@ -412,6 +425,41 @@ export function createWSControlServer(port) {
});
}

function requestExecJsInTestIframe(code, instanceName) {
return new Promise((resolve, reject) => {
const resolved = _resolveClient(instanceName);
if (resolved.error) {
reject(new Error(resolved.error));
return;
}

const { client } = resolved;
if (client.ws.readyState !== 1) {
reject(new Error("Phoenix client \"" + resolved.name + "\" is not connected"));
return;
}

const id = ++requestIdCounter;
const timeout = setTimeout(() => {
pendingRequests.delete(id);
reject(new Error("exec_js_in_test_iframe request timed out (30s)"));
}, 30000);

pendingRequests.set(id, {
resolve: (data) => {
clearTimeout(timeout);
resolve(data);
},
reject: (err) => {
clearTimeout(timeout);
reject(err);
}
});

client.ws.send(JSON.stringify({ type: "exec_js_in_test_iframe_request", id, code }));
});
}

function requestRunTests(category, spec, instanceName) {
return new Promise((resolve, reject) => {
const resolved = _resolveClient(instanceName);
Expand Down Expand Up @@ -538,6 +586,7 @@ export function createWSControlServer(port) {
requestLogs,
requestExecJs,
requestExecJsLivePreview,
requestExecJsInTestIframe,
requestRunTests,
requestTestResults,
getBrowserLogs,
Expand Down
21 changes: 19 additions & 2 deletions src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,21 @@ exports.answerQuestion = async function (params) {
return { success: true };
};

/**
* Resume a previous session by setting the session ID.
* The next sendPrompt call will use queryOptions.resume with this session ID.
*/
exports.resumeSession = async function (params) {
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
_questionResolve = null;
_queuedClarification = null;
currentSessionId = params.sessionId;
return { success: true };
};

/**
* Destroy the current session (clear session ID).
*/
Expand Down Expand Up @@ -832,11 +847,13 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,

if (isAbort) {
_log("Cancelled");
// Query was cancelled — clear session so next query starts fresh
// Send sessionId so browser side can save partial history for later resume
const cancelledSessionId = currentSessionId;
// Clear session so next query starts fresh
currentSessionId = null;
nodeConnector.triggerPeer("aiComplete", {
requestId: requestId,
sessionId: null
sessionId: cancelledSessionId
});
return;
}
Expand Down
1 change: 1 addition & 0 deletions src/brackets.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ define(function (require, exports, module) {
SidebarView: require("project/SidebarView"),
WorkingSetView: require("project/WorkingSetView"),
AISnapshotStore: require("core-ai/AISnapshotStore"),
AIChatHistory: require("core-ai/AIChatHistory"),
doneLoading: false
};

Expand Down
Loading
Loading