From 8617aa52937d76c67098f219ee53d593ddf1c6b4 Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Sat, 1 Aug 2026 17:11:52 +0200 Subject: [PATCH] gui: render user-interrupt with yellow background and warning icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Interrupt block type to ChatBlock enum - Detect and tags as block openers/closers in the parser - Render interrupt blocks with bright yellow header (#3d2e00 dark, #fff8e1 light) - Show ⚠ User Interrupt header label --- gui/chatblockmodel.cpp | 31 +++++++++++++++++++++++++++---- gui/chatblockmodel.h | 3 +++ gui/main.qml | 24 ++++++++++++++---------- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/gui/chatblockmodel.cpp b/gui/chatblockmodel.cpp index 3292275..6c9d441 100644 --- a/gui/chatblockmodel.cpp +++ b/gui/chatblockmodel.cpp @@ -49,6 +49,12 @@ const QRegularExpression ChatBlockModel::s_headerRe( const QRegularExpression ChatBlockModel::s_endRe( R"(^\[\[\[end\]\]\]$)"); +const QRegularExpression ChatBlockModel::s_interruptOpenRe( + R"(^\s*$)"); + +const QRegularExpression ChatBlockModel::s_interruptCloseRe( + R"(^\s*$)"); + ChatBlockModel::ChatBlockModel(QObject *parent) : QAbstractListModel(parent) { @@ -74,10 +80,11 @@ QVariant ChatBlockModel::data(const QModelIndex &index, int role) const case ChatBlock::Reasoning: return QStringLiteral("reasoning"); case ChatBlock::Call: return QStringLiteral("call"); case ChatBlock::Tool: return QStringLiteral("tool"); - case ChatBlock::Error: return QStringLiteral("error"); - case ChatBlock::Info: return QStringLiteral("info"); - case ChatBlock::Retry: return QStringLiteral("retry"); - case ChatBlock::Default: return QStringLiteral("default"); +case ChatBlock::Error: return QStringLiteral("error"); + case ChatBlock::Info: return QStringLiteral("info"); + case ChatBlock::Retry: return QStringLiteral("retry"); + case ChatBlock::Interrupt: return QStringLiteral("interrupt"); + case ChatBlock::Default: return QStringLiteral("default"); } return {}; case BlockNameRole: @@ -235,6 +242,12 @@ void ChatBlockModel::processLine(const QString &line) return; } + // --- Interrupt close tag --- + if (s_interruptCloseRe.match(line).hasMatch()) { + m_inBlock = false; + return; + } + // --- NoFence state: look for opening fence or header --- static const QRegularExpression fenceOpenRe(R"(^```(\w*)[ \t]*$)"); QRegularExpressionMatch fenceMatch = fenceOpenRe.match(line); @@ -254,6 +267,16 @@ void ChatBlockModel::processLine(const QString &line) return; } + // Interrupt open tag — creates its own block + if (s_interruptOpenRe.match(line).hasMatch()) { + beginInsertRows(QModelIndex(), m_blocks.size(), m_blocks.size()); + m_blocks.append({ChatBlock::Interrupt, QString(), QString(), QString()}); + endInsertRows(); + emit countChanged(); + m_inBlock = true; + return; + } + // Header line QRegularExpressionMatch m = s_headerRe.match(line); if (m.hasMatch()) { diff --git a/gui/chatblockmodel.h b/gui/chatblockmodel.h index 5412076..8673248 100644 --- a/gui/chatblockmodel.h +++ b/gui/chatblockmodel.h @@ -16,6 +16,7 @@ struct ChatBlock { Error, Info, Retry, + Interrupt, Default }; @@ -75,6 +76,8 @@ private: static const QRegularExpression s_headerRe; static const QRegularExpression s_endRe; + static const QRegularExpression s_interruptOpenRe; + static const QRegularExpression s_interruptCloseRe; }; #endif // CHATBLOCKMODEL_H diff --git a/gui/main.qml b/gui/main.qml index bf0f8b2..d0fa219 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -727,16 +727,17 @@ delegate: ColumnLayout { width: chatView.width - 8 spacing: 2 - property bool blockExpanded: { - switch (blockType) { - case "user": return true - case "assistant": return true - case "tool": return true - case "info": return true - case "retry": return true - default: return false - } - } +property bool blockExpanded: { + switch (blockType) { + case "user": return true + case "assistant": return true + case "tool": return true + case "info": return true + case "retry": return true + case "interrupt": return true + default: return false + } + } // Clickable header Rectangle { @@ -755,6 +756,7 @@ delegate: ColumnLayout { case "error": return Qt.rgba(0.6, 0.15, 0.15, 0.7) case "info": return Qt.lighter(bg, 1.4) case "retry": return Qt.rgba(0.6, 0.5, 0.0, 0.5) + case "interrupt": return "#3d2e00" default: return bg } } else { @@ -767,6 +769,7 @@ delegate: ColumnLayout { case "error": return Qt.rgba(1.0, 0.8, 0.8, 0.7) case "info": return Qt.darker(bg, 1.02) case "retry": return Qt.rgba(1.0, 0.9, 0.6, 0.5) + case "interrupt": return "#fff8e1" default: return bg } } @@ -801,6 +804,7 @@ delegate: ColumnLayout { case "error": return "Error" case "info": return "Info" case "retry": return "Retry" + case "interrupt": return "\u26A0 User Interrupt" default: return "" } }