gui: render user-interrupt with yellow background and warning icon

- Add Interrupt block type to ChatBlock enum
- Detect <system-user-interruption> and </system-user-interruption>
  tags as block openers/closers in the parser
- Render interrupt blocks with bright yellow header
  (#3d2e00 dark, #fff8e1 light)
- Show ⚠ User Interrupt header label
This commit is contained in:
Levi Neely 2026-08-01 17:11:52 +02:00
parent ada6dc1feb
commit 8617aa5293
3 changed files with 44 additions and 14 deletions

View File

@ -49,6 +49,12 @@ const QRegularExpression ChatBlockModel::s_headerRe(
const QRegularExpression ChatBlockModel::s_endRe(
R"(^\[\[\[end\]\]\]$)");
const QRegularExpression ChatBlockModel::s_interruptOpenRe(
R"(^<system-user-interruption>\s*$)");
const QRegularExpression ChatBlockModel::s_interruptCloseRe(
R"(^</system-user-interruption>\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()) {

View File

@ -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

View File

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