diff --git a/PreCompiledHeaders.cmake b/PreCompiledHeaders.cmake
index e0a4d70d639d15cec9971433e9744d44b9a3af31..b04fef6941a16f4b677b8d7bbb1604ff09ff989b 100644
--- a/PreCompiledHeaders.cmake
+++ b/PreCompiledHeaders.cmake
@@ -139,6 +139,7 @@ set(QT_WIDGET_INC
     <QFileIconProvider>
     <QFontDatabase>
     <QTextCodec>
+    <QListWidget>
 )
 
 set(Vivy_PRECOMPILED_INC PRIVATE
diff --git a/src/Lib/Log.hh b/src/Lib/Log.hh
index 428fb35879cc48849da4d9cd91cea82fcd0e3b60..063187a9ea52e5e79fb8a84d0f2fc99c59c4c557 100644
--- a/src/Lib/Log.hh
+++ b/src/Lib/Log.hh
@@ -197,6 +197,13 @@ public:
         return dispatch;
     }
 
+    template <Derived<LogSinkDispatcher> DispatcherType, typename... Args>
+    DispatcherType *newUnmanagedDispatcher(Args &&...args) noexcept
+    {
+        DispatcherType *dispatch = new DispatcherType(std::forward<Args>(args)...);
+        return dispatch;
+    }
+
     std::shared_ptr<Logger> newLogger(const StringType auto &category) noexcept
     {
         std::shared_ptr<Logger> logger = std::make_shared<Logger>(this, category);
diff --git a/src/UI/LogConsole.cc b/src/UI/LogConsole.cc
new file mode 100644
index 0000000000000000000000000000000000000000..7428a1a6a569d0c48b3f3994a5b19d82c338fd87
--- /dev/null
+++ b/src/UI/LogConsole.cc
@@ -0,0 +1,55 @@
+#include "LogConsole.hh"
+
+using namespace Vivy;
+
+ConsoleLogSinkDispatcher::ConsoleLogSinkDispatcher(QWidget *parent) noexcept
+    : LogSinkDispatcher(std::string_view{ "console" })
+    , QListWidget(parent)
+{
+    setSortingEnabled(false);
+    setSelectionMode(QAbstractItemView::NoSelection);
+
+    const QString style = QStringLiteral("* { font-family: \"FiraCode\"; font-size: 8pt; }");
+    setStyleSheet(style);
+}
+
+void
+ConsoleLogSinkDispatcher::setMessageCountLimit(int limit)
+{
+    if (limit <= 0)
+        throw std::logic_error("Can't pass a negative count!");
+    messageLimit = limit;
+}
+
+void
+ConsoleLogSinkDispatcher::handleLogMessage(const std::string_view cat,
+                                           const LogMessage &msg) noexcept
+{
+    while (count() >= messageLimit) {
+        QListWidgetItem *itemWidget = item(0);
+        if (itemWidget != nullptr)
+            removeItemWidget(itemWidget);
+    }
+
+    QString label;
+
+    // Magic number to reduce early allocations
+    label.reserve(std::max(static_cast<int>(cat.size()) * 2, 10));
+
+    label.append('[');
+    for (const char c : cat)
+        label.append(c);
+    label.append("] ");
+
+    for (const char c : LogLevel::toStdStringView(msg.getHeader().severity))
+        label.append(c);
+    label.append(" -> ");
+
+    // Add the content of the message
+    label.reserve(static_cast<int>(1 + label.size() +
+                                   std::max(10, static_cast<int>(msg.getTextBuffer().size()))));
+    for (const char c : msg.getTextBuffer())
+        label.append(c);
+
+    addItem(label);
+}
diff --git a/src/UI/LogConsole.hh b/src/UI/LogConsole.hh
new file mode 100644
index 0000000000000000000000000000000000000000..2525f10e86a7ba936e13566bff1efe2673652005
--- /dev/null
+++ b/src/UI/LogConsole.hh
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "Utils.hh"
+#include "../Lib/Log.hh"
+
+namespace Vivy
+{
+class ConsoleLogSinkDispatcher final : public LogSinkDispatcher, public QListWidget {
+    VIVY_UNMOVABLE_OBJECT(ConsoleLogSinkDispatcher)
+
+    int messageLimit{ 1'000 };
+
+public:
+    explicit ConsoleLogSinkDispatcher(QWidget *parent = nullptr) noexcept;
+    void handleLogMessage(const std::string_view, const LogMessage &) noexcept override;
+
+    void setMessageCountLimit(int limit);
+};
+}
diff --git a/src/UI/MainWindow.cc b/src/UI/MainWindow.cc
index b0f5f030dd731b6b1147400dec82d487bde3fa15..83b281bd1f27ca84b690e3029b6d4ea607574a6e 100644
--- a/src/UI/MainWindow.cc
+++ b/src/UI/MainWindow.cc
@@ -152,14 +152,11 @@ MainWindow::MainWindow() noexcept
     enableConnection(loadSubDocumentVideoAct, enableLoadSubOnDocument);
     enableConnection(loadSubDocumentAudioAct, enableLoadSubOnDocument);
 
-    // Main window has finished its construction
     statusBar()->showMessage("QSimulate has started");
 
-    // Minimal size...
     setMinimumHeight(400);
     setMinimumWidth(600);
-
-    // Always a new empty document
+    setUnifiedTitleAndToolBarOnMac(true);
     newDocument();
 }
 
diff --git a/src/VivyApplication.cc b/src/VivyApplication.cc
index 286bd28edcb979c67c99e80725c31594df765c64..de244c0a3cffb4b231736bcdef17e8578990c96f 100644
--- a/src/VivyApplication.cc
+++ b/src/VivyApplication.cc
@@ -1,5 +1,6 @@
 #include "VivyApplication.hh"
 #include "UI/MainWindow.hh"
+#include "UI/DockWidgetTitleBar.hh"
 #include "Lib/Document/VivyDocumentStore.hh"
 #include "Lib/Script/ScriptStore.hh"
 
@@ -40,8 +41,6 @@ VivyApplication::setTheme(Theme theme) noexcept
         setStyleSheet(stylesheetStream.readAll());
         logInfo() << "Theme set using " << VIVY_LOG_QUOTED(sheet);
     }
-
-    flushLogSink();
 }
 
 int
@@ -82,14 +81,22 @@ VivyApplication::exec() noexcept
     }
 
     case ApplicationType::GUI: {
-        // Show the main window
-        mainWindowPtr = std::make_shared<MainWindow>();
+        // Show the main window, also set up the log console
+        mainWindowPtr               = std::make_shared<MainWindow>();
+        QDockWidget *logConsoleDock = new QDockWidget("Console", mainWindowPtr.get());
+        std::shared_ptr<ConsoleLogSinkDispatcher> consoleLogSinkDispatcher =
+            logSink->newDispatcher<ConsoleLogSinkDispatcher>(nullptr);
+
+        DockWidgetTitleBar::addToDock(logConsoleDock);
+        logConsoleDock->setAllowedAreas(Qt::BottomDockWidgetArea);
+        logConsoleDock->setFeatures(QDockWidget::DockWidgetMovable);
+        logConsoleDock->setWidget(consoleLogSinkDispatcher.get());
+        mainWindowPtr->addDockWidget(Qt::BottomDockWidgetArea, logConsoleDock);
         mainWindowPtr->show();
 
         logInfo() << "Entering the main event loop";
 
         // Main loop
-        flushLogSink();
         return QApplication::exec();
     }
     }
diff --git a/src/VivyApplication.hh b/src/VivyApplication.hh
index 18b9cacec08da43437758cdca2e0662551ac4a52..7317c7ea752bfea182e02d36a48bcf82281dd74a 100644
--- a/src/VivyApplication.hh
+++ b/src/VivyApplication.hh
@@ -40,6 +40,7 @@
 #endif
 
 #include "Lib/Log.hh"
+#include "UI/LogConsole.hh"
 
 namespace Vivy
 {