diff --git a/gulpfile.js/validate-build.js b/gulpfile.js/validate-build.js
index 1f7bad73ea..95417a7adf 100644
--- a/gulpfile.js/validate-build.js
+++ b/gulpfile.js/validate-build.js
@@ -29,7 +29,7 @@ const DEV_MAX_TOTAL_SIZE_MB = 100;
// Custom size limits for known large files (size in MB) For development builds
const LARGE_FILE_LIST_DEV = {
'dist/thirdparty/no-minify/language-worker.js.map': 10,
- 'dist/brackets-min.js': 15
+ 'dist/brackets-min.js': 20
};
// Size limits for production/staging builds (in MB)
diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js
index 3380e2e778..05475298c8 100644
--- a/src-node/claude-code-agent.js
+++ b/src-node/claude-code-agent.js
@@ -663,15 +663,34 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
"controlEditor). Never use relative paths." +
"\n\nWhen a tool response mentions the user has typed a clarification, immediately " +
"call getUserClarification to read it and incorporate the user's feedback into your current work." +
- "\n\nYou are running inside Phoenix Code, a web-focused code editor with a built-in " +
- "live preview for HTML/CSS/JS. When the user asks to create mockups, prototypes, " +
- "or web pages, prefer vanilla HTML/CSS/JS so the live preview can render and " +
- "edit them — unless the user specifically requests a framework. " +
- "Build responsive layouts by default for web content." +
- "\n\nWhen planning, consider if verification is needed. takeScreenshot can " +
- "capture the full editor, specific panels, the code area, or the live preview. " +
- "For HTML/CSS/JS with live preview, execJsInLivePreview can run JS in the " +
- "browser to confirm behavior." +
+ "\n\nYou are running inside Phoenix Code, a web-focused code editor with built-in " +
+ "live preview for both HTML/CSS/JS/SVG and Markdown. When the user asks to create " +
+ "mockups, prototypes, or web pages, prefer vanilla HTML/CSS/JS so the live preview " +
+ "can render and edit them — unless the user specifically requests a framework. " +
+ "Build responsive layouts by default for web content. For images, prefer real " +
+ " tags over div background-image so the user can swap, inspect, and resize " +
+ "them in the editor — only fall back to background-image when an effect (parallax, " +
+ "cover-with-overlay, repeating tile) genuinely requires it." +
+ "\n\nYou can debug and inspect the live preview directly — these tools are for " +
+ "active iteration, not just final verification:" +
+ "\n- takeScreenshot: see the rendered HTML preview, the rendered Markdown preview, " +
+ "the editor, or any panel. Use it to confirm visual output, diagnose layout/styling " +
+ "bugs, or check that HTML or Markdown rendered as expected. Pass reload=true to " +
+ "force-reload the preview before capturing (useful after JS edits) — saves a tool " +
+ "call vs. reloading separately." +
+ "\n- execJsInLivePreview: run JS inside the HTML preview iframe to read the DOM, " +
+ "query computed styles, click elements, or capture console output. Use it to debug " +
+ "behavior, not just to verify." +
+ "\n- resizeLivePreview: change the preview viewport width to test responsive " +
+ "breakpoints." +
+ "\n- controlEditor: open files, move the cursor, change selection, toggle the live " +
+ "preview panel, or reload it (reloadLivePreview operation — use after JS edits if " +
+ "you're not also taking a screenshot)." +
+ "\n- getEditorState: report active file, working set, cursor/selection, and the " +
+ "livePreviewFile. The live preview normally follows the active editor file, so " +
+ "assume that. Rarely the user pins the preview to a specific file — if a " +
+ "screenshot doesn't match the file you just edited, check " +
+ "getEditorState.livePreviewFile to rule that out." +
"\n\nUse your best judgement for when to enter plan mode. Use it when the task " +
"involves creating new applications, extensive modifications, or architectural " +
"changes — propose a plan for user approval before writing code." +
diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js
index 4896a598f1..269eb605ac 100644
--- a/src-node/mcp-editor-tools.js
+++ b/src-node/mcp-editor-tools.js
@@ -128,10 +128,13 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors)
"or '.editor-holder' to capture only the code editor area. " +
"Only omit the selector when you need to see the full editor application layout. " +
"Note: live preview screenshots may include Phoenix toolbox overlays on selected elements. " +
- "Use purePreview=true to temporarily hide these overlays and render the page as it would appear in a real browser.",
+ "Use purePreview=true to temporarily hide these overlays and render the page as it would appear in a real browser. " +
+ "Use reload=true to force-reload the live preview before capturing — useful after editing JS, " +
+ "and saves a tool call vs. calling controlEditor.reloadLivePreview separately.",
{
selector: z.string().optional().describe("CSS selector to capture a specific element. Use '#panel-live-preview-frame' for the preview panel (HTML live preview or markdown preview), '.editor-holder' for the code editor."),
purePreview: z.boolean().optional().describe("When true, temporarily switches to preview mode to hide element highlight overlays and toolboxes before capturing, then restores the previous mode."),
+ reload: z.boolean().optional().describe("When true, force-reloads the live preview before capturing. Use this instead of a separate reloadLivePreview call when you're about to screenshot anyway."),
filePath: z.string().optional().describe("Absolute path to save the screenshot as a PNG file. If specified, returns the file path instead of inline image data.")
},
async function (args) {
@@ -140,6 +143,7 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors)
const result = await _execPeerWithTimeout(nodeConnector, "takeScreenshot", {
selector: args.selector || undefined,
purePreview: args.purePreview || false,
+ reload: args.reload || false,
filePath: args.filePath || undefined
}, "takeScreenshot");
if (result.filePath) {
@@ -212,10 +216,14 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors)
"- openInWorkingSet: Open a file and pin it to the working set. Params: filePath\n" +
"- setSelection: Open a file and select a range. Params: filePath, startLine, startCh, endLine, endCh\n" +
"- setCursorPos: Open a file and set cursor position. Params: filePath, line, ch\n" +
- "- toggleLivePreview: Show or hide the live preview panel. Params: show (boolean)",
+ "- toggleLivePreview: Show or hide the live preview panel. Params: show (boolean)\n" +
+ "- reloadLivePreview: Force-reload the live preview iframe (and any popped-out preview tabs). " +
+ "Use after editing JS that doesn't appear to have hot-reloaded. Note: if you're about to call " +
+ "takeScreenshot anyway, prefer takeScreenshot({ reload: true }) — it reloads and captures in " +
+ "one step. No params.",
{
operations: z.array(z.object({
- operation: z.enum(["open", "close", "openInWorkingSet", "setSelection", "setCursorPos", "toggleLivePreview"]),
+ operation: z.enum(["open", "close", "openInWorkingSet", "setSelection", "setCursorPos", "toggleLivePreview", "reloadLivePreview"]),
filePath: z.string().optional().describe("Absolute path to the file (not required for toggleLivePreview)"),
startLine: z.number().optional().describe("Start line (1-based) for setSelection"),
startCh: z.number().optional().describe("Start column (1-based) for setSelection"),
diff --git a/src/extensionsIntegrated/Phoenix/phoenix-tour.js b/src/extensionsIntegrated/Phoenix/phoenix-tour.js
index 2a0f156960..7eee1aadd8 100644
--- a/src/extensionsIntegrated/Phoenix/phoenix-tour.js
+++ b/src/extensionsIntegrated/Phoenix/phoenix-tour.js
@@ -32,6 +32,12 @@ define(function (require, exports, module) {
const Strings = require("strings"),
StringUtils = require("utils/StringUtils"),
Metrics = require("utils/Metrics"),
+ SidebarView = require("project/SidebarView"),
+ ProjectManager = require("project/ProjectManager"),
+ EditorManager = require("editor/EditorManager"),
+ CommandManager = require("command/CommandManager"),
+ Commands = require("command/Commands"),
+ WorkspaceManager = require("view/WorkspaceManager"),
CentralControlBar = require("view/CentralControlBar");
// Capture the kernel trust ring at module-load time — it's deleted from
@@ -46,13 +52,6 @@ define(function (require, exports, module) {
const STEP_START_DELAY_MS = 2500;
const STEP1_INVITE_MS = 1800;
const STEP1_DESIGN_MODE_HOLD_MS = 2000;
- // Hard cap on how long we'll wait for the pro trial start dialog to be
- // dismissed before starting the tour. The dialog is shown on every fresh
- // first-run boot (where this tour also runs), so under normal conditions
- // the wait is bounded by the user dismissing it. The cap protects edge
- // cases where the dialog isn't shown at all (e.g. user already has a
- // subscription / a prior expired trial).
- const TRIAL_DIALOG_WAIT_TIMEOUT_MS = 60000;
function _loadState() {
const raw = PhStore.getItem(TOUR_STORAGE_KEY);
@@ -101,7 +100,7 @@ define(function (require, exports, module) {
}
}
- const TOTAL_STEPS = 3;
+ const TOTAL_STEPS = 4;
function _ensureOverlay() {
if ($overlay) {
@@ -189,7 +188,21 @@ define(function (require, exports, module) {
update();
}
+ /**
+ * Make sure the sidebar is showing before each step. Upgrade flows can
+ * boot Phoenix with the sidebar hidden (the user's last-session state),
+ * which would hide the AI tab and the new-project button this tour
+ * points at. Cheap to call when already visible — SidebarView.show()
+ * is a no-op then.
+ */
+ function _ensureSidebarVisible() {
+ if (SidebarView && SidebarView.isVisible && !SidebarView.isVisible()) {
+ SidebarView.show();
+ }
+ }
+
function _runStep1() {
+ _ensureSidebarVisible();
const $btn = $("#ccbCollapseEditorBtn");
if (!$btn.length) {
_markComplete();
@@ -235,6 +248,7 @@ define(function (require, exports, module) {
}
function _runStep2() {
+ _ensureSidebarVisible();
const $tab = $('.sidebar-tab[data-tab-id="ai"]');
if (!$tab.length) {
// No AI tab in this build — skip ahead to the next step.
@@ -260,11 +274,12 @@ define(function (require, exports, module) {
}
function _runStep3() {
+ _ensureSidebarVisible();
const $newBtn = $("#newProject");
if (!$newBtn.length) {
- // No new-project button — tour is effectively done.
- _markComplete();
- _teardown();
+ // No new-project button — skip to the live-preview step instead
+ // of giving up on the tour entirely.
+ _runStep4();
return;
}
_ensureOverlay();
@@ -272,6 +287,95 @@ define(function (require, exports, module) {
_setStep(3);
Metrics.countEvent(Metrics.EVENT_TYPE.GUIDE, "tour", "step3");
_setText(Strings.PHOENIX_TOUR_NEW_PROJECT);
+ _setActions([
+ {
+ label: Strings.PHOENIX_TOUR_NEXT_BTN,
+ kind: "primary",
+ onClick: function () {
+ _runStep4();
+ }
+ }
+ ]);
+ // Intentionally do NOT advance on a real click of the target — only
+ // the Next button advances.
+ }
+
+ /**
+ * Bring the workspace to a state where the live-preview Edit Mode button
+ * is visible and meaningful: the welcome project must be open, the
+ * active editor file must be one the preview can render (the welcome
+ * project's index.html or phoenix-pro.html), and the LP panel must be
+ * showing. Each branch is a no-op when already true.
+ */
+ async function _ensureLivePreviewReady() {
+ const welcomeRoot = ProjectManager.getWelcomeProjectPath();
+
+ // 1. Switch to the welcome project if we're not already there.
+ const currentRoot = ProjectManager.getProjectRoot();
+ if (!currentRoot || currentRoot.fullPath !== welcomeRoot) {
+ await new Promise(function (resolve) {
+ ProjectManager.openProject(welcomeRoot)
+ .done(resolve).fail(resolve);
+ });
+ }
+
+ // 2. Make sure the active file is one the LP can render. Prefer
+ // phoenix-pro.html (Pro flow) and fall back to index.html.
+ const proPath = welcomeRoot + "phoenix-pro.html";
+ const indexPath = welcomeRoot + "index.html";
+ const editor = EditorManager.getActiveEditor();
+ const currentFile = editor && editor.document && editor.document.file
+ ? editor.document.file.fullPath : null;
+ if (currentFile !== proPath && currentFile !== indexPath) {
+ let target = null;
+ try {
+ if (await Phoenix.VFS.existsAsync(proPath)) {
+ target = proPath;
+ } else if (await Phoenix.VFS.existsAsync(indexPath)) {
+ target = indexPath;
+ }
+ } catch (e) { /* fall through with target=null */ }
+ if (target) {
+ await new Promise(function (resolve) {
+ CommandManager.execute(Commands.FILE_OPEN, { fullPath: target })
+ .done(resolve).fail(resolve);
+ });
+ }
+ }
+
+ // 3. Open the live-preview panel if it isn't visible.
+ const lpPanel = WorkspaceManager.getPanelForID("live-preview-panel");
+ if (!lpPanel || !lpPanel.isVisible()) {
+ await new Promise(function (resolve) {
+ CommandManager.execute(Commands.FILE_LIVE_FILE_PREVIEW)
+ .done(resolve).fail(resolve);
+ });
+ }
+ }
+
+ async function _runStep4() {
+ _ensureSidebarVisible();
+ try {
+ await _ensureLivePreviewReady();
+ } catch (e) { /* best-effort prep — proceed and let the rect-zero
+ RAF gate sort out a missing target */ }
+
+ const $btn = $("#previewModeLivePreviewButton");
+ if (!$btn.length) {
+ // LP panel never came up (custom server, unsupported file, etc.)
+ // — finalize the tour rather than stalling on a missing target.
+ _markComplete();
+ _teardown();
+ return;
+ }
+ _ensureOverlay();
+ // LP panel sits on the right edge; keep the default tooltip
+ // placement (lower-right of the ring) so the tooltip extends back
+ // into the panel area rather than off the right edge of the viewport.
+ _trackTarget($btn, "left");
+ _setStep(4);
+ Metrics.countEvent(Metrics.EVENT_TYPE.GUIDE, "tour", "step4");
+ _setText(Strings.PHOENIX_TOUR_EDIT_MODE);
_setActions([
{
label: Strings.PHOENIX_TOUR_DISMISS_BTN,
@@ -283,8 +387,6 @@ define(function (require, exports, module) {
}
}
]);
- // Intentionally do NOT end on a real click of the target — only the
- // Dismiss button ends the tour.
}
function _shouldRun() {
@@ -308,36 +410,19 @@ define(function (require, exports, module) {
}
/**
- * Resolves once the pro trial start dialog has been dismissed, or after
- * TRIAL_DIALOG_WAIT_TIMEOUT_MS as a fallback for builds/runs where the
- * dialog isn't shown.
+ * Resolves once the pro trial start dialog has been dismissed. The
+ * dialog is guaranteed to fire `proTrialStartDialogDismissed` on every
+ * boot path (including builds where the dialog isn't shown), so we
+ * just await it without a timeout fallback.
*/
function _waitForTrialStartDialogDismissed() {
- return new Promise(function (resolve) {
- const dismissed = _LoginService && _LoginService.proTrialStartDialogDismissed;
- if (!dismissed) {
- // No pro trial flow exposed — proceed immediately.
- resolve();
- return;
- }
- let settled = false;
- const fallback = setTimeout(function () {
- if (settled) {
- return;
- }
- settled = true;
- resolve();
- }, TRIAL_DIALOG_WAIT_TIMEOUT_MS);
- // jQuery deferred or native promise — both implement .then
- Promise.resolve(dismissed).then(function () {
- if (settled) {
- return;
- }
- settled = true;
- clearTimeout(fallback);
- resolve();
- });
- });
+ const dismissed = _LoginService && _LoginService.proTrialStartDialogDismissed;
+ // Community-edition builds expose no login service at all — skip
+ // the wait so the tour still works there.
+ if (!dismissed) {
+ return Promise.resolve();
+ }
+ return Promise.resolve(dismissed);
}
function startTour() {
diff --git a/src/htmlContent/about-dialog.html b/src/htmlContent/about-dialog.html
index 021c3ade52..2a98fd61a4 100644
--- a/src/htmlContent/about-dialog.html
+++ b/src/htmlContent/about-dialog.html
@@ -19,6 +19,7 @@