diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js
index 0e375a209..64302d2fb 100644
--- a/src/public/app/layouts/desktop_layout.js
+++ b/src/public/app/layouts/desktop_layout.js
@@ -79,6 +79,7 @@ import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
 import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
 import MermaidExportButton from "../widgets/floating_buttons/mermaid_export_button.js";
 import EditableCodeButtonsWidget from "../widgets/type_widgets/editable_code_buttons.js";
+import ApiLogWidget from "../widgets/api_log.js";
 
 export default class DesktopLayout {
     constructor(customWidgets) {
@@ -197,6 +198,7 @@ export default class DesktopLayout {
                                         .child(new SqlResultWidget())
                                 )
                                 .child(new EditableCodeButtonsWidget())
+                                .child(new ApiLogWidget())
                                 .child(new FindWidget())
                                 .child(
                                     ...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC
diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js
index 5ae0eaa05..6036c9c5b 100644
--- a/src/public/app/services/frontend_script_api.js
+++ b/src/public/app/services/frontend_script_api.js
@@ -13,6 +13,7 @@ import appContext from "./app_context.js";
 import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
 import NoteContextCachingWidget from "../widgets/note_context_caching_widget.js";
 import BasicWidget from "../widgets/basic_widget.js";
+import SpacedUpdate from "./spaced_update.js";
 
 /**
  * This is the main frontend API interface for scripts. It's published in the local "api" object.
@@ -594,6 +595,33 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
      * @returns {string} random string
      */
     this.randomString = utils.randomString;
+
+    this.logMessages = {};
+    this.logSpacedUpdates = {};
+
+    /**
+     * Log given message to the log pane in UI
+     *
+     * @param message
+     */
+    this.log = message => {
+        const {noteId} = this.startNote;
+
+        message = utils.now() + ": " + message;
+
+        console.log(`Script ${noteId}: ${message}`);
+
+        this.logMessages[noteId] = this.logMessages[noteId] || [];
+        this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
+            const messages = this.logMessages[noteId];
+            this.logMessages[noteId] = [];
+
+            appContext.triggerEvent("apiLogMessages", {noteId, messages});
+        }, 100);
+
+        this.logMessages[noteId].push(message);
+        this.logSpacedUpdates[noteId].scheduleUpdate();
+    };
 }
 
 export default FrontendScriptApi;
diff --git a/src/public/app/services/ws.js b/src/public/app/services/ws.js
index 113d1f0ad..6dd34c1c6 100644
--- a/src/public/app/services/ws.js
+++ b/src/public/app/services/ws.js
@@ -3,6 +3,7 @@ import toastService from "./toast.js";
 import server from "./server.js";
 import options from "./options.js";
 import frocaUpdater from "./froca_updater.js";
+import appContext from "./app_context.js";
 
 const messageHandlers = [];
 
@@ -118,6 +119,9 @@ async function handleMessage(event) {
     else if (message.type === 'consistency-checks-failed') {
         toastService.showError("Consistency checks failed! See logs for details.", 50 * 60000);
     }
+    else if (message.type === 'api-log-messages') {
+        appContext.triggerEvent("apiLogMessages", {noteId: message.noteId, messages: message.messages});
+    }
 }
 
 let entityChangeIdReachedListeners = [];
diff --git a/src/public/app/widgets/api_log.js b/src/public/app/widgets/api_log.js
new file mode 100644
index 000000000..72d12691a
--- /dev/null
+++ b/src/public/app/widgets/api_log.js
@@ -0,0 +1,54 @@
+import NoteContextAwareWidget from "./note_context_aware_widget.js";
+
+const TPL = `
+
`;
+
+export default class ApiLogWidget extends NoteContextAwareWidget {
+    isEnabled() {
+        return this.note
+            && this.note.mime.startsWith('application/javascript;env=')
+            && super.isEnabled();
+    }
+
+    doRender() {
+        this.$widget = $(TPL);
+        this.$widget.addClass("hidden-api-log");
+
+        this.$logContainer = this.$widget.find('.api-log-container');
+    }
+
+    async refreshWithNote(note) {
+        this.$logContainer.empty();
+    }
+
+    apiLogMessagesEvent({messages, noteId}) {
+        if (!this.isNote(noteId)) {
+            return;
+        }
+
+        this.$widget.removeClass("hidden-api-log");
+
+        for (const message of messages) {
+            this.$logContainer.append(message).append($("
"));
+        }
+    }
+}
diff --git a/src/public/app/widgets/script_log.js b/src/public/app/widgets/script_log.js
deleted file mode 100644
index 681a14d48..000000000
--- a/src/public/app/widgets/script_log.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import NoteContextAwareWidget from "./note_context_aware_widget.js";
-
-const TPL = `
-`;
-
-export default class ScriptLogWidget extends NoteContextAwareWidget {
-    isEnabled() {
-        return this.note
-            && this.note.mime.startsWith('application/javascript;env=')
-            && super.isEnabled();
-    }
-
-    doRender() {
-        this.$widget = $(TPL);
-
-        this.$logContainer = this.$widget.find('.script-log-container');
-    }
-}
diff --git a/src/public/app/widgets/type_widgets/editable_code_buttons.js b/src/public/app/widgets/type_widgets/editable_code_buttons.js
index 6386fde43..006ace51f 100644
--- a/src/public/app/widgets/type_widgets/editable_code_buttons.js
+++ b/src/public/app/widgets/type_widgets/editable_code_buttons.js
@@ -87,4 +87,10 @@ export default class EditableCodeButtonsWidget extends NoteContextAwareWidget {
 
         this.$openTriliumApiDocsButton.toggle(note.mime.startsWith('application/javascript;env='));
     }
+
+    async noteTypeMimeChangedEvent({noteId}) {
+        if (this.isNote(noteId)) {
+            await this.refresh();
+        }
+    }
 }
diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js
index c457bbaf6..0f4f378d8 100644
--- a/src/services/backend_script_api.js
+++ b/src/services/backend_script_api.js
@@ -14,6 +14,8 @@ const appInfo = require('./app_info');
 const searchService = require('./search/services/search');
 const SearchContext = require("./search/search_context");
 const becca = require("../becca/becca");
+const ws = require("./ws");
+const SpacedUpdate = require("./spaced_update");
 
 /**
  * This is the main backend API interface for scripts. It's published in the local "api" object.
@@ -288,12 +290,34 @@ function BackendScriptApi(currentNote, apiParams) {
         });
     };
 
+    this.logMessages = {};
+    this.logSpacedUpdates = {};
+
     /**
-     * Log given message to trilium logs.
+     * Log given message to trilium logs and log pane in UI
      *
      * @param message
      */
-    this.log = message => log.info(message);
+    this.log = message => {
+        log.info(message);
+
+        const {noteId} = this.startNote;
+
+        this.logMessages[noteId] = this.logMessages[noteId] || [];
+        this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
+            const messages = this.logMessages[noteId];
+            this.logMessages[noteId] = [];
+
+            ws.sendMessageToAllClients({
+                type: 'api-log-messages',
+                noteId,
+                messages
+            });
+        }, 100);
+
+        this.logMessages[noteId].push(message);
+        this.logSpacedUpdates[noteId].scheduleUpdate();
+    };
 
     /**
      * Returns root note of the calendar.
diff --git a/src/services/spaced_update.js b/src/services/spaced_update.js
new file mode 100644
index 000000000..d038a09db
--- /dev/null
+++ b/src/services/spaced_update.js
@@ -0,0 +1,67 @@
+class SpacedUpdate {
+    constructor(updater, updateInterval = 1000) {
+        this.updater = updater;
+        this.lastUpdated = Date.now();
+        this.changed = false;
+        this.updateInterval = updateInterval;
+    }
+
+    scheduleUpdate() {
+        if (!this.changeForbidden) {
+            this.changed = true;
+            setTimeout(() => this.triggerUpdate());
+        }
+    }
+
+    async updateNowIfNecessary() {
+        if (this.changed) {
+            this.changed = false; // optimistic...
+
+            try {
+                await this.updater();
+            }
+            catch (e) {
+                this.changed = true;
+
+                throw e;
+            }
+        }
+    }
+
+    isAllSavedAndTriggerUpdate() {
+        const allSaved = !this.changed;
+
+        this.updateNowIfNecessary();
+
+        return allSaved;
+    }
+
+    triggerUpdate() {
+        if (!this.changed) {
+            return;
+        }
+
+        if (Date.now() - this.lastUpdated > this.updateInterval) {
+            this.updater();
+            this.lastUpdated = Date.now();
+            this.changed = false;
+        }
+        else {
+            // update not triggered but changes are still pending so we need to schedule another check
+            this.scheduleUpdate();
+        }
+    }
+
+    async allowUpdateWithoutChange(callback) {
+        this.changeForbidden = true;
+
+        try {
+            await callback();
+        }
+        finally {
+            this.changeForbidden = false;
+        }
+    }
+}
+
+module.exports = SpacedUpdate;
diff --git a/src/services/ws.js b/src/services/ws.js
index 0231b200d..4581e8942 100644
--- a/src/services/ws.js
+++ b/src/services/ws.js
@@ -67,7 +67,7 @@ function sendMessageToAllClients(message) {
     const jsonStr = JSON.stringify(message);
 
     if (webSocketServer) {
-        if (message.type !== 'sync-failed') {
+        if (message.type !== 'sync-failed' && message.type !== 'api-log-messages') {
             log.info("Sending message to all clients: " + jsonStr);
         }