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 ""
}
}