diff --git a/package-lock.json b/package-lock.json
index ba77c58a85..48f4ee8dfd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "phoenix",
- "version": "5.1.6-0",
+ "version": "5.1.7-0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "phoenix",
- "version": "5.1.6-0",
+ "version": "5.1.7-0",
"hasInstallScript": true,
"dependencies": {
"@bugsnag/js": "^7.18.0",
diff --git a/phoenix-builder-mcp/package-lock.json b/phoenix-builder-mcp/package-lock.json
index 64359b6072..e19671cbc0 100644
--- a/phoenix-builder-mcp/package-lock.json
+++ b/phoenix-builder-mcp/package-lock.json
@@ -370,7 +370,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -575,7 +574,6 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz",
"integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -1146,7 +1144,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/src-mdviewer/src/bridge.js b/src-mdviewer/src/bridge.js
index dfd25b292a..a998a76ea0 100644
--- a/src-mdviewer/src/bridge.js
+++ b/src-mdviewer/src/bridge.js
@@ -8,7 +8,7 @@ import { getState, setState } from "./core/state.js";
import { setLocale } from "./core/i18n.js";
import { marked } from "marked";
import * as docCache from "./core/doc-cache.js";
-import { broadcastSelectionStateSync } from "./components/editor.js";
+import { broadcastSelectionStateSync, flushPendingContentChange } from "./components/editor.js";
let _syncId = 0;
let _lastReceivedSyncId = -1;
@@ -224,6 +224,9 @@ export function initBridge() {
window.__broadcastSelectionStateForTest = function () {
broadcastSelectionStateSync();
};
+ window.__saveScrollPos = function () {
+ docCache.saveActiveScrollPos();
+ };
window.__triggerContentSync = function () {
const content = document.getElementById("viewer-content");
if (content) {
@@ -269,6 +272,9 @@ export function initBridge() {
case "MDVIEWR_SET_THEME":
handleSetTheme(data);
break;
+ case "MDVIEWR_SET_PRO_STATUS":
+ setState({ isPro: !!data.isPro });
+ break;
case "MDVIEWR_SET_EDIT_MODE":
handleSetEditMode(data);
break;
@@ -509,8 +515,11 @@ export function initBridge() {
entry.mdSrc = markdown;
}
}
- // Send cursor position BEFORE the edit for undo restore
- sendToParent("mdviewrContentChanged", { markdown, _syncId, cursorPos: _cursorPosBeforeEdit });
+ // Send cursor position BEFORE the edit for undo restore.
+ // Include file path so MarkdownSync can verify the change matches the active document.
+ sendToParent("mdviewrContentChanged", {
+ markdown, _syncId, cursorPos: _cursorPosBeforeEdit, filePath: activePath
+ });
_cursorPosDirty = false; // allow cursor tracking again
});
@@ -576,6 +585,7 @@ function handleSetContent(data) {
_baseURL = baseURL;
}
+ flushPendingContentChange();
_suppressContentChange = true;
const parseResult = parseMarkdownToHTML(markdown);
@@ -665,6 +675,12 @@ function handleSwitchFile(data) {
_baseURL = baseURL;
}
+ // Flush any pending debounced content-change from the outgoing file's edits
+ // BEFORE suppressing. This ensures the outgoing file's cache entry and
+ // currentContent are updated with the latest edits, preventing data loss
+ // when the user switches files quickly (within the 50ms debounce window).
+ flushPendingContentChange();
+
_suppressContentChange = true;
// Suppress scroll-to-line from CM during file switch — the doc cache
diff --git a/src-mdviewer/src/components/editor.js b/src-mdviewer/src/components/editor.js
index aadd1d08b8..5492bd5ae4 100644
--- a/src-mdviewer/src/components/editor.js
+++ b/src-mdviewer/src/components/editor.js
@@ -1714,6 +1714,23 @@ function emitContentChange(contentEl) {
}, CONTENT_CHANGE_DEBOUNCE);
}
+/**
+ * Flush any pending debounced content-change emission immediately.
+ * Called during file switch so the outgoing file's edits are synced
+ * to its cache entry and CM document before switching away.
+ */
+export function flushPendingContentChange() {
+ if (contentChangeTimer) {
+ clearTimeout(contentChangeTimer);
+ contentChangeTimer = null;
+ const contentEl = document.getElementById("viewer-content");
+ if (contentEl) {
+ const markdown = convertToMarkdown(contentEl);
+ emit("bridge:contentChanged", { markdown });
+ }
+ }
+}
+
function getContentEl() {
return document.getElementById("viewer-content");
}
diff --git a/src-mdviewer/src/components/embedded-toolbar.js b/src-mdviewer/src/components/embedded-toolbar.js
index aa3f01699e..4589a98e75 100644
--- a/src-mdviewer/src/components/embedded-toolbar.js
+++ b/src-mdviewer/src/components/embedded-toolbar.js
@@ -34,7 +34,8 @@ import {
Image as ImageIcon,
Upload,
Sun,
- Moon
+ Moon,
+ Crown
} from "lucide";
import { on, emit } from "../core/events.js";
import { getState, setState } from "../core/state.js";
@@ -57,7 +58,7 @@ const _isMacWebKit = /Mac/.test(navigator.platform)
&& !/Chrome|CriOS|Edg|Firefox|FxiOS/.test(navigator.userAgent);
const allIcons = { Bold, Italic, Strikethrough, Underline, Code, Link, List, ListOrdered,
- ListChecks, Quote, Minus, Table, FileCode, ChevronDown, Type, MoreHorizontal, Pencil, BookOpen, Link2, Link2Off, Printer, Image: ImageIcon, Upload, Sun, Moon };
+ ListChecks, Quote, Minus, Table, FileCode, ChevronDown, Type, MoreHorizontal, Pencil, BookOpen, Link2, Link2Off, Printer, Image: ImageIcon, Upload, Sun, Moon, Crown };
export function initEmbeddedToolbar() {
toolbar = document.getElementById("toolbar");
@@ -67,6 +68,7 @@ export function initEmbeddedToolbar() {
on("state:editMode", () => render());
on("state:theme", () => render());
+ on("state:isPro", () => render());
on("editor:selection-state", updateFormatState);
on("state:locale", () => render());
}
@@ -102,9 +104,10 @@ function renderReadMode() {
-