diff --git a/gui/chatblockmodel.cpp b/gui/chatblockmodel.cpp index b2ab05e..3292275 100644 --- a/gui/chatblockmodel.cpp +++ b/gui/chatblockmodel.cpp @@ -44,7 +44,10 @@ private: // --- Block Parsing --- const QRegularExpression ChatBlockModel::s_headerRe( - R"(^\[(user|assistant|reasoning|call|tool|error|info|retry)(?::([^\]]*))?\]$)"); + R"(^\[\[\[(user|assistant|reasoning|call|tool|error|info|retry)(?::([^\]]*))?\]\]\]$)"); + +const QRegularExpression ChatBlockModel::s_endRe( + R"(^\[\[\[end\]\]\]$)"); ChatBlockModel::ChatBlockModel(QObject *parent) : QAbstractListModel(parent) @@ -118,6 +121,7 @@ void ChatBlockModel::reset() m_fenceState = NoFence; m_fenceLang.clear(); m_fenceBuffer.clear(); + m_inBlock = false; endResetModel(); emit countChanged(); } @@ -225,6 +229,12 @@ void ChatBlockModel::processLine(const QString &line) return; } + // --- End block marker --- + if (s_endRe.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); @@ -265,16 +275,23 @@ void ChatBlockModel::processLine(const QString &line) m_blocks.append({type, qualifier, QString(), QString()}); endInsertRows(); emit countChanged(); + m_inBlock = true; return; } - // Content line — append to the last block if any - if (m_blocks.isEmpty()) { - // Mid-stream connect: create a Default block - beginInsertRows(QModelIndex(), 0, 0); - m_blocks.append({ChatBlock::Default, QString(), QString(), QString()}); - endInsertRows(); - emit countChanged(); + // Content line — only append if inside a block or connecting mid-stream + if (!m_inBlock) { + // Dropped content between blocks — ignore to avoid accidental parsing. + // Mid-stream connect: if no blocks exist yet, create a default container. + if (m_blocks.isEmpty()) { + beginInsertRows(QModelIndex(), 0, 0); + m_blocks.append({ChatBlock::Default, QString(), QString(), QString()}); + endInsertRows(); + emit countChanged(); + m_inBlock = true; + } else { + return; + } } ChatBlock &last = m_blocks.last(); diff --git a/gui/chatblockmodel.h b/gui/chatblockmodel.h index a832e06..5412076 100644 --- a/gui/chatblockmodel.h +++ b/gui/chatblockmodel.h @@ -60,6 +60,11 @@ private: QString m_fenceLang; QString m_fenceBuffer; + // Block state machine — tracks whether we're inside a [[[type]]]...[[[end]]] block. + // Content lines outside a block are ignored, making the format robust against + // accidental header-like content in the wild. + bool m_inBlock = false; + // Syntax highlighting (shared with Kate plugin pattern) QString highlightCode(const QString &code, const QString &language) const; QString renderDiff(const QString &diff) const; @@ -69,6 +74,7 @@ private: QString m_lineBuffer; static const QRegularExpression s_headerRe; + static const QRegularExpression s_endRe; }; #endif // CHATBLOCKMODEL_H diff --git a/gui/main.qml b/gui/main.qml index 2f79439..705bca3 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -207,7 +207,7 @@ ApplicationWindow { buildSessionTree() } function onStreamingDone() { - chatModel.appendData("\n[info]\nsession ended\n") + chatModel.appendData("\n[[[info]]]\nsession ended\n[[[end]]]\n") } }