chat log: switch to [[[type]]]/[[[end]]] block format

Replace the single-bracket INI-like format with triple-bracket blocks
with explicit [[[end]]] terminators. The parser tracks m_inBlock to
ignore content between blocks, making the format robust against
accidental header-like content in tool output or LLM responses.

- Go sender: FormatEvent + NewEventHandler + replayMessagesToLog
- C++ parser: new regex for [[[type]]], new [[[end]]] handler, m_inBlock gate
- QML: update hardcoded [info] string
This commit is contained in:
Levi Neely 2026-08-01 16:48:57 +02:00
parent b13dc6064b
commit 998efc15c5
3 changed files with 32 additions and 9 deletions

View File

@ -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();

View File

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

View File

@ -207,7 +207,7 @@ ApplicationWindow {
buildSessionTree()
}
function onStreamingDone() {
chatModel.appendData("\n[info]\nsession ended\n")
chatModel.appendData("\n[[[info]]]\nsession ended\n[[[end]]]\n")
}
}