From 3729bb1dda6a31b8edaca68293677bea313e39a2 Mon Sep 17 00:00:00 2001
From: Xayton <30591904+Xayton@users.noreply.github.com>
Date: Mon, 29 Apr 2024 18:26:35 +0200
Subject: [PATCH 1/2] Update the home.html file that powers the "Debug
 Console": - Improved the CSS code and JS code - Added the support for the
 "show 'list'" options as a select - Add an additional section to show only
 the "list" commands, when the user selects such option - Fixed the code
 detecting "list" messages, as it was missing cases where the JSON content had
 a "Ports" property but its value was null instead of an empty array.

---
 home.html | 476 +++++++++++++++++++++++++++++++-----------------------
 1 file changed, 270 insertions(+), 206 deletions(-)

diff --git a/home.html b/home.html
index 30c241f22..796dc1a8b 100644
--- a/home.html
+++ b/home.html
@@ -1,38 +1,53 @@
 <!DOCTYPE html>
 <html lang="en">
+
 <head>
-<title>Arduino Create Agent Debug Console</title>
-<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=swap" rel="stylesheet">
-<link href="https://fonts.googleapis.com/css?family=Roboto+Mono:400,600,700&display=swap" rel="stylesheet">
-<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
-<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
-<script type="text/javascript">
-    $(function() {
-	    var socket;
-	    var input = $('#input');
-	    var log = document.getElementById('log');
-	    var autoscroll = document.getElementById('autoscroll');
-	    var listenabled = document.getElementById('list');
-	    var messages = [];
-        var MESSAGES_MAX_COUNT = 2000;
-
-	    function appendLog(msg) {
-            let jsonMsg = {};
-            let portListing = false;
-            try {
-                jsonMsg = JSON.parse(msg);
-                portsListing = jsonMsg.Ports;
-            } catch {
-                // no valid json
-            }
+    <title>Arduino Create Agent Debug Console</title>
+    <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=swap" rel="stylesheet">
+    <link href="https://fonts.googleapis.com/css?family=Roboto+Mono:400,600,700&display=swap" rel="stylesheet">
+    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
+    <script type="text/javascript"
+        src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
+    <script type="text/javascript">
+        $(function () {
+            var socket;
+            var input = $('#input');
+            var log = document.getElementById('log');
+            var logList = document.getElementById('log-list');
+
+            var autoscroll = document.getElementById('autoscroll');
+            var messages = [];
+            const MESSAGES_MAX_COUNT = 2000;
+
+            // Check if the list visibility setting is saved in the localStorage.
+            const LOCAL_STORAGE_KEY = "ArduinoAgentListVisibility";
+            let savedSetting = localStorage.getItem(LOCAL_STORAGE_KEY);
+            let listCommand = savedSetting != null ? parseInt(savedSetting) : 1; // Default: Show list commands inline.
+            document.getElementById('listShow').value = listCommand;
+            updateLogVisibility();
+
+            function appendLog(msg) {
+                let jsonMsg = {};
+                let portsListing = false;
+                try {
+                    // Try to parse the received message as JSON, and then check if it contains a "Ports" property.
+                    jsonMsg = JSON.parse(msg);
+                    portsListing = jsonMsg.Ports !== undefined;
+                } catch {
+                    // no valid json
+                }
+
+                // This is a "list" message if it starts with "list" or if it's valid JSON and has a "Ports" property.
+                let isListMessage = portsListing || msg.indexOf('list') == 0;
+
+                // If this is a "LIST" command and we want to hide them. Skip.
+                if (isListMessage && listCommand == 0) return;
 
-            var startsWithList = msg.indexOf('list') == 0;
 
-            if (listenabled.checked || (!portsListing && !startsWithList)) {
                 let printMsg = msg;
                 if (jsonMsg.Ports) {
                     const validKeys = ['Name', 'SerialNumber', 'IsOpen', 'VendorID', 'ProductID'];
-                    printMsg = "Serial Ports:\n"+JSON.stringify(jsonMsg.Ports, validKeys, 2);
+                    printMsg = "Serial Ports:\n" + JSON.stringify(jsonMsg.Ports, validKeys, 2);
                 } else if (Object.keys(jsonMsg).length !== 0) {
                     printMsg = JSON.stringify(jsonMsg, undefined, 2);
                 }
@@ -45,192 +60,241 @@
                 }
                 printMsg = decode(printMsg);
 
-                messages.push(printMsg);
-                if (messages.length > MESSAGES_MAX_COUNT) {
-                    messages.shift();
+
+                // Check if this is a "LIST" message and needs to be shown in a separate log.
+                if (isListMessage && listCommand == 2) {
+                    // Show the "list" message in the specific log element. Replace any previous content.
+                    logList.textContent = "list\n\n" + printMsg;
+
+                } else {
+                    // Show the message in the log element.
+                    messages.push(printMsg);
+                    if (messages.length > MESSAGES_MAX_COUNT) {
+                        messages.shift();
+                    }
+                    log.textContent = messages.join('\n\n');
+                    if (autoscroll.checked) {
+                        log.scrollTop = log.scrollHeight - log.clientHeight;
+                    }
+                }
+            }
+
+            $('#form').submit(function (e) {
+                e.preventDefault();
+                if (!socket) {
+                    return false;
+                }
+                if (!input.val()) {
+                    return false;
+                }
+                socket.emit('command', input.val());
+                input.val('');
+            });
+
+            $('#export').click(function () {
+                var link = document.createElement('a');
+                link.setAttribute('download', 'agent-log.txt');
+                var text = log.textContent;
+                link.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
+                link.click();
+            });
+
+            $('#clear').click(function () {
+                messages = [];
+                log.innerHTML = '';
+                logList.innerHTML = '';
+            });
+
+            $('#listShow').change(function () {
+                listCommand = document.getElementById('listShow').value;
+                localStorage.setItem(LOCAL_STORAGE_KEY, "" + listCommand);
+                updateLogVisibility();
+            });
+
+            function updateLogVisibility() {
+                if (listCommand == 2) {
+                    logList.innerHTML = '';
+                    $("#log-list").show();
+                } else {
+                    $("#log-list").hide();
                 }
-                log.textContent = messages.join('\n\n');
-                if (autoscroll.checked) {
-                    log.scrollTop = log.scrollHeight - log.clientHeight;
+            }
+
+            if (window['WebSocket']) {
+                if (window.location.protocol === 'https:') {
+                    socket = io('https://{{$}}')
+                } else {
+                    socket = io('http://{{$}}');
                 }
+                socket.on('disconnect', function (evt) {
+                    appendLog('Connection closed.')
+                });
+                socket.on('message', function (evt) {
+                    appendLog(evt);
+                });
+            } else {
+                appendLog('Your browser does not support WebSockets.')
             }
-	    }
-
-	    $('#form').submit(function(e) {
-	    	e.preventDefault();
-	        if (!socket) {
-	            return false;
-	        }
-	        if (!input.val()) {
-	            return false;
-	        }
-	        socket.emit('command', input.val());
-            input.val('');
-	    });
-
-	    $('#export').click(function() {
-	    	var link = document.createElement('a');
-	    	link.setAttribute('download', 'agent-log.txt');
-	    	var text = log.textContent;
-	    	link.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
-    		link.click();
-    	});
-
-        $('#clear').click(function() {
-            messages = [];
-            log.innerHTML = '';
+
+            $("#input").focus();
         });
+    </script>
+    <style type="text/css">
+        html,
+        body {
+            overflow: hidden;
+            height: 100%;
+        }
+
+        body {
+            margin: 0px;
+            padding: 0px;
+            background: #F8F9F9;
+            font-size: 16px;
+            font-family: "Open Sans", "Lucida Grande", Lucida, Verdana, sans-serif;
+        }
+
+        #container {
+            display: flex;
+            flex-direction: column;
+            height: 100vh;
+            width: 100%;
+        }
+
+        .logs {
+            display: flex;
+            gap: 10px;
+
+            flex: 1;
+            overflow-y: auto;
+            margin: 15px 15px 10px;
+
+            font-family: "Roboto Mono", "Courier", "Lucida Grande", Verdana, sans-serif;
+        }
+
+        .logs pre {
+            background-color: #DAE3E3;
+            padding: 8px 10px;
+            margin: 0;
+            overflow-y: auto;
+        }
+
+        #log {
+            flex-basis: 65%;
+            flex-grow: 1;
+        }
+
+        #log-list {
+            flex-basis: 35%;
+        }
+
+        #footer {
+            display: flex;
+            flex-wrap: wrap;
+            align-items: flex-start;
+            justify-content: space-between;
+            margin: 0px 15px 0px;
+        }
+
+        #form {
+            display: flex;
+            flex-grow: 1;
+            margin-bottom: 15px;
+        }
+
+        #input {
+            flex-grow: 1;
+        }
+
+        #secondary-controls div {
+            display: inline-block;
+            padding: 10px 15px;
+        }
 
-	    if (window['WebSocket']) {
-	        if (window.location.protocol === 'https:') {
-	            socket = io('https://{{$}}')
-	        } else {
-	            socket = io('http://{{$}}');
-	        }
-	        socket.on('disconnect', function(evt) {
-	            appendLog('Connection closed.')
-	        });
-	        socket.on('message', function(evt) {
-	            appendLog(evt);
-	        });
-	    } else {
-	        appendLog('Your browser does not support WebSockets.')
-        }
-
-        $("#input").focus();
-	});
-</script>
-<style type="text/css">
-html, body {
-    overflow: hidden;
-    height: 100%;
-}
-
-body {
-    margin: 0px;
-    padding: 0px;
-    background: #F8F9F9;
-    font-size: 16px;
-    font-family: "Open Sans", "Lucida Grande", Lucida, Verdana, sans-serif;
-}
-
-#container {
-    display: flex;
-    flex-direction: column;
-    height: 100vh;
-    width: 100%;
-}
-
-#log {
-    flex-grow: 1;
-    font-family: "Roboto Mono", "Courier", "Lucida Grande", Verdana, sans-serif;
-    background-color: #DAE3E3;
-    height: calc(100vh - 61px);
-    margin: 15px 15px 10px;
-    padding: 8px 10px;
-    overflow-y: auto;
-}
-
-#footer {
-    display: flex;
-    flex-wrap: wrap;
-    align-items: flex-start;
-    justify-content: space-between;
-    margin: 0px 15px 0px;
-}
-
-#form {
-    display: flex;
-    flex-grow: 1;
-    margin-bottom: 15px;
-}
-
-#input {
-    flex-grow: 1;
-}
-
-#secondary-controls div {
-    display: inline-block;
-    padding: 10px 15px;
-}
-
-#autoscroll,
-#list {
-    vertical-align: middle;
-    width: 20px;
-    height: 20px;
-}
-
-
-#secondary-controls button {
-    margin-bottom: 15px;
-    vertical-align: top;
-}
-
-.button {
-    background-color: #b5c8c9;
-    border: 1px solid #b5c8c9;
-    border-radius: 2px 2px 0 0;
-    box-shadow: 0 4px #95a5a6;
-    margin-bottom: 4px;
-    color: #000;
-    cursor: pointer;
-    font-size: 14px;
-    letter-spacing: 1.28px;
-    line-height: normal;
-    outline: none;
-    padding: 9px 18px;
-    text-align: center;
-    text-transform: uppercase;
-    transition: box-shadow .1s ease-out, transform .1s ease-out;
-}
-
-.button:hover {
-    box-shadow: 0 2px #95a5a6;
-    outline: none;
-    transform: translateY(2px);
-}
-
-.button:active {
-    box-shadow: none;
-    transform: translateY(4px);
-}
-
-.textfield {
-    background-color: #dae3e3;
-    width: auto;
-    height: auto;
-    padding: 10px 8px;
-    margin-left: 8px;
-    vertical-align: top;
-    border: none;
-    font-family: "Open Sans", "Lucida Grande", Lucida, Verdana, sans-serif;
-    font-size: 1em;
-    outline: none;
-}
-</style>
+        #autoscroll,
+        #list {
+            vertical-align: middle;
+            width: 20px;
+            height: 20px;
+        }
+
+
+        #secondary-controls .button {
+            margin-bottom: 15px;
+            vertical-align: top;
+            height: 36px;
+        }
+
+        .button {
+            background-color: #b5c8c9;
+            border: 1px solid #b5c8c9;
+            border-radius: 2px 2px 0 0;
+            box-shadow: 0 4px #95a5a6;
+            margin-bottom: 4px;
+            color: #000;
+            cursor: pointer;
+            font-size: 14px;
+            letter-spacing: 1.28px;
+            line-height: normal;
+            outline: none;
+            padding: 9px 18px;
+            text-align: center;
+            text-transform: uppercase;
+            transition: box-shadow .1s ease-out, transform .1s ease-out;
+        }
+
+        .button:hover {
+            box-shadow: 0 2px #95a5a6;
+            outline: none;
+            transform: translateY(2px);
+        }
+
+        .button:active {
+            box-shadow: none;
+            transform: translateY(4px);
+        }
+
+        .textfield {
+            background-color: #dae3e3;
+            width: auto;
+            height: auto;
+            padding: 10px 8px;
+            margin-left: 8px;
+            vertical-align: top;
+            border: none;
+            font-family: "Open Sans", "Lucida Grande", Lucida, Verdana, sans-serif;
+            font-size: 1em;
+            outline: none;
+        }
+    </style>
 </head>
-    <body>
-        <div id="container">
+
+<body>
+    <div id="container">
+        <div class="logs">
             <pre id="log"></pre>
-            <div id="footer">
-                <form id="form">
-                    <input type="submit" class="button" value="Send" />
-                    <input type="text" id="input" class="textfield" aria-label="send command" />
-                </form>
-                <div id="secondary-controls">
-                    <div>
-                        <input name="pause" type="checkbox" checked id="autoscroll" />
-                        <label for="autoscroll">Autoscroll</label>
-                    </div>
-                    <div>
-                        <input name="list" type="checkbox" checked id="list" />
-                        <label for="list">Enable&nbsp;List&nbsp;Command</label>
-                    </div>
-                    <button id="clear" class="button">Clear&nbsp;Log</button>
-                    <button id="export" class="button">Export&nbsp;Log</button>
+            <pre id="log-list"></pre>
+        </div>
+        <div id="footer">
+            <form id="form">
+                <input type="submit" class="button" value="Send" />
+                <input type="text" id="input" class="textfield" aria-label="send command" autocomplete="off" />
+            </form>
+            <div id="secondary-controls">
+                <div>
+                    <input name="pause" type="checkbox" checked id="autoscroll" />
+                    <label for="autoscroll">Autoscroll</label>
                 </div>
+                <select name="listShow" id="listShow" class="button">
+                    <option value="0">Hide 'list' commands</option>
+                    <option value="1" selected>Show 'list' commands inline</option>
+                    <option value="2">Show 'list' commands separately</option>
+                </select>
+                <button id="clear" class="button">Clear&nbsp;Log</button>
+                <button id="export" class="button">Export&nbsp;Log</button>
             </div>
         </div>
-    </body>
-</html>
+    </div>
+</body>
+
+</html>
\ No newline at end of file

From 2b3e8ae9b439a8d4913350427b35a5b5569df3a7 Mon Sep 17 00:00:00 2001
From: Xayton <30591904+Xayton@users.noreply.github.com>
Date: Tue, 30 Apr 2024 16:36:10 +0200
Subject: [PATCH 2/2] The Debug Console is now jQuery-free. The code has been
 restructured to be simpler and avoid global variable as much as possible.

---
 home.html | 233 +++++++++++++++++++++++++++++-------------------------
 1 file changed, 127 insertions(+), 106 deletions(-)

diff --git a/home.html b/home.html
index 796dc1a8b..e66b7c975 100644
--- a/home.html
+++ b/home.html
@@ -5,121 +5,64 @@
     <title>Arduino Create Agent Debug Console</title>
     <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=swap" rel="stylesheet">
     <link href="https://fonts.googleapis.com/css?family=Roboto+Mono:400,600,700&display=swap" rel="stylesheet">
-    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
-    <script type="text/javascript"
-        src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
-    <script type="text/javascript">
-        $(function () {
-            var socket;
-            var input = $('#input');
-            var log = document.getElementById('log');
-            var logList = document.getElementById('log-list');
-
-            var autoscroll = document.getElementById('autoscroll');
-            var messages = [];
-            const MESSAGES_MAX_COUNT = 2000;
-
-            // Check if the list visibility setting is saved in the localStorage.
-            const LOCAL_STORAGE_KEY = "ArduinoAgentListVisibility";
-            let savedSetting = localStorage.getItem(LOCAL_STORAGE_KEY);
-            let listCommand = savedSetting != null ? parseInt(savedSetting) : 1; // Default: Show list commands inline.
-            document.getElementById('listShow').value = listCommand;
-            updateLogVisibility();
-
-            function appendLog(msg) {
-                let jsonMsg = {};
-                let portsListing = false;
-                try {
-                    // Try to parse the received message as JSON, and then check if it contains a "Ports" property.
-                    jsonMsg = JSON.parse(msg);
-                    portsListing = jsonMsg.Ports !== undefined;
-                } catch {
-                    // no valid json
-                }
-
-                // This is a "list" message if it starts with "list" or if it's valid JSON and has a "Ports" property.
-                let isListMessage = portsListing || msg.indexOf('list') == 0;
-
-                // If this is a "LIST" command and we want to hide them. Skip.
-                if (isListMessage && listCommand == 0) return;
-
-
-                let printMsg = msg;
-                if (jsonMsg.Ports) {
-                    const validKeys = ['Name', 'SerialNumber', 'IsOpen', 'VendorID', 'ProductID'];
-                    printMsg = "Serial Ports:\n" + JSON.stringify(jsonMsg.Ports, validKeys, 2);
-                } else if (Object.keys(jsonMsg).length !== 0) {
-                    printMsg = JSON.stringify(jsonMsg, undefined, 2);
-                }
-
-                // when parsing JSON we're escaping some html charaters like "&<>", we want to show their
-                // original value in the log
-                function decode(str) {
-                    let txt = new DOMParser().parseFromString(str, "text/html");
-                    return txt.documentElement.textContent;
-                }
-                printMsg = decode(printMsg);
+    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
 
+    <script type="text/javascript">
+        const LOCAL_STORAGE_KEY = "ArduinoAgentListVisibility";
+        let messages = []; // The messages to show in the main log.
 
-                // Check if this is a "LIST" message and needs to be shown in a separate log.
-                if (isListMessage && listCommand == 2) {
-                    // Show the "list" message in the specific log element. Replace any previous content.
-                    logList.textContent = "list\n\n" + printMsg;
+        document.addEventListener("DOMContentLoaded", function () {
+            let listMsgVisibility = getListMsgVisibility();
+            updateListMsgVisibility(listMsgVisibility);
 
-                } else {
-                    // Show the message in the log element.
-                    messages.push(printMsg);
-                    if (messages.length > MESSAGES_MAX_COUNT) {
-                        messages.shift();
-                    }
-                    log.textContent = messages.join('\n\n');
-                    if (autoscroll.checked) {
-                        log.scrollTop = log.scrollHeight - log.clientHeight;
-                    }
-                }
-            }
+            const socket = setupWebsocket();
 
-            $('#form').submit(function (e) {
-                e.preventDefault();
+            // Handle the form submission and send the message to the websocket.
+            document.getElementById("form").addEventListener("submit", function (event) {
+                event.preventDefault(); // Stop the from from actually submitting.
                 if (!socket) {
                     return false;
                 }
-                if (!input.val()) {
+
+                let input = document.getElementById("input");
+                if (!input.value) {
                     return false;
                 }
-                socket.emit('command', input.val());
-                input.val('');
+                socket.emit('command', input.value);
+                input.value = '';
             });
+        });
 
-            $('#export').click(function () {
-                var link = document.createElement('a');
-                link.setAttribute('download', 'agent-log.txt');
-                var text = log.textContent;
-                link.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
-                link.click();
-            });
 
-            $('#clear').click(function () {
-                messages = [];
-                log.innerHTML = '';
-                logList.innerHTML = '';
-            });
+        function getListMsgVisibility() {
+            // Check if the list visibility setting is saved in the localStorage.
+            let savedSetting = localStorage.getItem(LOCAL_STORAGE_KEY);
+            let listCommand = savedSetting != null ? parseInt(savedSetting) : 1; // Default: Show list commands inline.
 
-            $('#listShow').change(function () {
-                listCommand = document.getElementById('listShow').value;
-                localStorage.setItem(LOCAL_STORAGE_KEY, "" + listCommand);
-                updateLogVisibility();
-            });
+            document.getElementById('listShow').value = listCommand;
+            return listCommand;
+        }
 
-            function updateLogVisibility() {
-                if (listCommand == 2) {
-                    logList.innerHTML = '';
-                    $("#log-list").show();
-                } else {
-                    $("#log-list").hide();
-                }
+        function onListMsgVisibilityChange() {
+            let listCommand = document.getElementById('listShow').value;
+            localStorage.setItem(LOCAL_STORAGE_KEY, "" + listCommand); // Save the setting for future use.
+
+            // Update the rest of the UI so that it reflects the selected setting.
+            updateListMsgVisibility(listCommand);
+        }
+
+        function updateListMsgVisibility(visibility) {
+            const element = document.getElementById('log-list');
+            if (visibility == 2) {
+                element.innerHTML = '';           // Clear the "list" log.
+                element.style.display = 'block';  // Make sure the "list" log UI is visible.
+            } else {
+                element.style.display = 'none';   // Make sure the "list" log UI is hidden.
             }
+        }
 
+        function setupWebsocket() {
+            let socket;
             if (window['WebSocket']) {
                 if (window.location.protocol === 'https:') {
                     socket = io('https://{{$}}')
@@ -135,9 +78,87 @@
             } else {
                 appendLog('Your browser does not support WebSockets.')
             }
+            return socket;
+        }
 
-            $("#input").focus();
-        });
+        function appendLog(msg) {
+            const MESSAGES_MAX_COUNT = 2000;
+
+            let jsonMsg = {};
+            let portsListing = false;
+            try {
+                // Try to parse the received message as JSON, and then check if it contains a "Ports" property.
+                jsonMsg = JSON.parse(msg);
+                portsListing = jsonMsg.Ports !== undefined;
+            } catch {
+                // no valid json
+            }
+
+            // This is a "list" message if it starts with "list" or if it's valid JSON and has a "Ports" property.
+            let isListMessage = portsListing || msg.indexOf('list') == 0;
+
+            // Get the current setting for the "list" message visibility.
+            const listMsgVisibility = document.getElementById('listShow').value;
+
+            // If this is a "LIST" command and we want to hide them. Skip.
+            if (isListMessage && listMsgVisibility == 0) return;
+
+
+            let printMsg = msg;
+            if (jsonMsg.Ports) {
+                const validKeys = ['Name', 'SerialNumber', 'IsOpen', 'VendorID', 'ProductID'];
+                printMsg = "Serial Ports:\n" + JSON.stringify(jsonMsg.Ports, validKeys, 2);
+            } else if (Object.keys(jsonMsg).length !== 0) {
+                printMsg = JSON.stringify(jsonMsg, undefined, 2);
+            }
+
+            // when parsing JSON we're escaping some html charaters like "&<>", we want to show their
+            // original value in the log
+            function decode(str) {
+                let txt = new DOMParser().parseFromString(str, "text/html");
+                return txt.documentElement.textContent;
+            }
+            printMsg = decode(printMsg);
+
+
+            const log1 = document.getElementById('log');
+            const log2 = document.getElementById('log-list');
+            const autoscroll = document.getElementById('autoscroll');
+
+            // Check if this is a "LIST" message and needs to be shown in a separate log.
+            if (isListMessage && listMsgVisibility == 2) {
+                // Show the "list" message in the specific log element. Replace any previous content.
+                log2.textContent = "list\n\n" + printMsg;
+
+            } else {
+                // Show the message in the log element.
+                messages.push(printMsg);
+                if (messages.length > MESSAGES_MAX_COUNT) {
+                    messages.shift();
+                }
+                log1.textContent = messages.join('\n\n');
+                if (autoscroll.checked) {
+                    log1.scrollTop = log1.scrollHeight - log1.clientHeight;
+                }
+            }
+        }
+
+        function clearLogs() {
+            const log1 = document.getElementById('log');
+            const log2 = document.getElementById('log-list');
+
+            messages = [];
+            log1.innerHTML = '';
+            log2.innerHTML = '';
+        }
+
+        function exportLogs() {
+            const link = document.createElement('a');
+            link.setAttribute('download', 'agent-log.txt');
+            const text = document.getElementById('log').textContent;
+            link.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
+            link.click();
+        }
     </script>
     <style type="text/css">
         html,
@@ -278,20 +299,20 @@
         <div id="footer">
             <form id="form">
                 <input type="submit" class="button" value="Send" />
-                <input type="text" id="input" class="textfield" aria-label="send command" autocomplete="off" />
+                <input type="text" id="input" class="textfield" autocomplete="off" autofocus />
             </form>
             <div id="secondary-controls">
                 <div>
-                    <input name="pause" type="checkbox" checked id="autoscroll" />
+                    <input type="checkbox" checked id="autoscroll" />
                     <label for="autoscroll">Autoscroll</label>
                 </div>
-                <select name="listShow" id="listShow" class="button">
+                <select id="listShow" class="button" onchange="onListMsgVisibilityChange()">
                     <option value="0">Hide 'list' commands</option>
                     <option value="1" selected>Show 'list' commands inline</option>
                     <option value="2">Show 'list' commands separately</option>
                 </select>
-                <button id="clear" class="button">Clear&nbsp;Log</button>
-                <button id="export" class="button">Export&nbsp;Log</button>
+                <button class="button" onclick="clearLogs()">Clear Log</button>
+                <button class="button" onclick="exportLogs()">Export Log</button>
             </div>
         </div>
     </div>