Simplify diagram rendering and document architecture

This commit is contained in:
Ollie Agent 2026-08-18 12:11:04 +02:00
parent 5069755aaa
commit 6819b5f391
3 changed files with 125 additions and 120 deletions

View File

@ -6,6 +6,47 @@
Ollie is an AI agent runtime built around a 9P virtual filesystem. The runtime service, `olliesrv`, exposes sessions, agents, prompts, state, history, tools, and control operations as files. Frontends remain thin clients of that interface.
```mermaid
flowchart TB
B["olliesrv<br/>9P namespace · session lifecycle · agent runtime<br/>prompt + history · backend dispatch"]
S1["Session A (toolsrv)<br/>host + cwd"]
S2["Session B (toolsrv)<br/>host + cwd"]
S3["Session C (toolsrv)<br/>host + cwd"]
SX["(...)"]
B --- S1
B --- S2
B --- S3
B --- SX
A1["Agent 1"]
A2["Agent 2"]
A3["Agent 3"]
A4["Agent 4"]
A5["Agent 5"]
A6["Agent 6"]
AN["Agent N"]
AX["(...)"]
S1 --- A1
S1 --- A2
S2 --- A3
S2 --- A4
S3 --- A5
S3 --- A6
SX --- AN
SX --- AX
classDef brain fill:#d66b3d,stroke:#642b1c,color:#fff,stroke-width:4px;
classDef session fill:#e7c65c,stroke:#67551b,color:#211b08;
classDef agent fill:#c7d4eb,stroke:#354a70,color:#172033;
class B brain;
class S1,S2,S3,SX session;
class A1,A2,A3,A4,A5,A6,AN,AX agent;
```
```text
frontends (shell, Acme, KDE, scripts)
│ 9P

View File

@ -238,7 +238,6 @@ void ChatBlockModel::reset()
m_fenceContent.clear();
m_pendingDiagrams.clear();
m_inFlightRequests.clear();
m_diagramCache.clear(); // Clear cache on reset to pick up fixes
endResetModel();
emit countChanged();
}
@ -258,12 +257,6 @@ void ChatBlockModel::trimFront(int keepCount)
emit countChanged();
}
void ChatBlockModel::clearDiagramCache()
{
m_diagramCache.clear();
qDebug() << "Diagram cache cleared";
}
QString ChatBlockModel::getDefaultTextColor() const
{
const auto textColor = QApplication::palette().color(QPalette::Text);
@ -375,56 +368,10 @@ void ChatBlockModel::rebuildBlockHtml(int blockIndex)
html += diagramPlaceholder(fenceLang);
seg.diagramLoading = true;
} else {
// Complete fence — check cache or request diagram
const QString cacheKey = fenceLang + QStringLiteral(":") +
QString::fromLatin1(QCryptographicHash::hash(
code.toUtf8(), QCryptographicHash::Md5).toHex());
if (m_diagramCache.contains(cacheKey)) {
const QByteArray cached = m_diagramCache.value(cacheKey);
if (cached.startsWith("ERROR:")) {
// Error: fall back to source code rendering (not a diagram segment)
QString codeHtml = highlightCode(code, fenceLang);
html += codeHtml;
BlockSegment textSeg;
textSeg.type = BlockSegment::Text;
textSeg.html = codeHtml;
segments.append(textSeg);
fenceLines.clear();
fenceLang.clear();
return; // Don't append diagram segment
} else {
// Check if JPEG, PNG, or SVG
if (cached.startsWith("JPEG:")) {
QByteArray jpegData = cached.mid(5); // Skip "JPEG:" prefix
html += diagramJpegHtml(jpegData);
seg.diagramSvg = QStringLiteral("data:image/jpeg;base64,") +
QString::fromLatin1(jpegData.toBase64());
} else if (cached.startsWith("PNG:")) {
QByteArray pngData = cached.mid(4); // Skip "PNG:" prefix
html += diagramPngHtml(pngData);
seg.diagramSvg = QStringLiteral("data:image/png;base64,") +
QString::fromLatin1(pngData.toBase64());
} else if (cached.startsWith("SVG:")) {
QByteArray svgData = cached.mid(4); // Skip "SVG:" prefix
html += diagramSvgHtml(svgData);
seg.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
QString::fromLatin1(svgData.toBase64());
} else {
// Legacy: assume SVG if no prefix
html += diagramSvgHtml(cached);
seg.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
QString::fromLatin1(cached.toBase64());
}
}
} else {
html += diagramPlaceholder(fenceLang);
seg.diagramLoading = true;
// Request diagram asynchronously
requestDiagram(fenceLang, code, blockIndex);
}
// Complete fence — render through the local diagram server.
html += diagramPlaceholder(fenceLang);
seg.diagramLoading = true;
requestDiagram(fenceLang, code, blockIndex);
}
segments.append(seg);
} else {
@ -568,40 +515,15 @@ void ChatBlockModel::incrementalAppend(int blockIndex, const QString &line)
rs.inFence = false;
const QString code = rs.fenceLines.join('\n');
if (isDiagramLanguage(rs.fenceLang)) {
// Replace last segment with a diagram segment
BlockSegment seg;
seg.type = BlockSegment::Diagram;
seg.diagramLang = rs.fenceLang;
seg.diagramSource = code;
seg.html = highlightCode(code, rs.fenceLang);
const QString cacheKey = rs.fenceLang + QStringLiteral(":") +
QString::fromLatin1(QCryptographicHash::hash(
code.toUtf8(), QCryptographicHash::Md5).toHex());
if (m_diagramCache.contains(cacheKey)) {
const QByteArray cached = m_diagramCache.value(cacheKey);
if (cached.startsWith("ERROR:")) {
// Error: keep as text segment with highlighted source
seg.type = BlockSegment::Text;
} else if (cached.startsWith("JPEG:")) {
seg.diagramSvg = QStringLiteral("data:image/jpeg;base64,") +
QString::fromLatin1(cached.mid(5).toBase64());
} else if (cached.startsWith("PNG:")) {
seg.diagramSvg = QStringLiteral("data:image/png;base64,") +
QString::fromLatin1(cached.mid(4).toBase64());
} else if (cached.startsWith("SVG:")) {
seg.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
QString::fromLatin1(cached.mid(4).toBase64());
} else {
seg.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
QString::fromLatin1(cached.toBase64());
}
} else {
if (isDiagramLanguage(rs.fenceLang)) {
// Replace last segment with a diagram segment
BlockSegment seg;
seg.type = BlockSegment::Diagram;
seg.diagramLang = rs.fenceLang;
seg.diagramSource = code;
seg.html = highlightCode(code, rs.fenceLang);
seg.diagramLoading = true;
requestDiagram(rs.fenceLang, code, blockIndex);
}
// Replace the in-progress fence segment
if (!block.segments.isEmpty())
@ -826,31 +748,22 @@ QString ChatBlockModel::diagramJpegHtml(const QByteArray &jpeg) const
void ChatBlockModel::requestDiagram(const QString &lang, const QString &code, int blockIndex)
{
const QString cacheKey = lang + QStringLiteral(":") +
const QString requestKey = lang + QStringLiteral(":") +
QString::fromLatin1(QCryptographicHash::hash(
code.toUtf8(), QCryptographicHash::Md5).toHex());
// Already have it cached?
if (m_diagramCache.contains(cacheKey))
// Track duplicate requests only while this local render is in flight.
if (!m_pendingDiagrams.contains(requestKey))
m_pendingDiagrams[requestKey] = QList<int>();
if (!m_pendingDiagrams[requestKey].contains(blockIndex))
m_pendingDiagrams[requestKey].append(blockIndex);
if (m_inFlightRequests.contains(requestKey))
return;
// Track which blocks need this diagram
if (!m_pendingDiagrams.contains(cacheKey)) {
m_pendingDiagrams[cacheKey] = QList<int>();
}
if (!m_pendingDiagrams[cacheKey].contains(blockIndex)) {
m_pendingDiagrams[cacheKey].append(blockIndex);
}
m_inFlightRequests.insert(requestKey);
// Already requesting?
if (m_inFlightRequests.contains(cacheKey))
return;
m_inFlightRequests.insert(cacheKey);
// Build request
QNetworkRequest request;
request.setAttribute(QNetworkRequest::User, cacheKey);
request.setAttribute(QNetworkRequest::User, requestKey);
request.setTransferTimeout(10000);
const QString langLower = lang.toLower();
@ -867,7 +780,7 @@ void ChatBlockModel::requestDiagram(const QString &lang, const QString &code, in
request.setUrl(QUrl(QStringLiteral("http://localhost:8081/img/") + QString::fromLatin1(encoded)));
m_netManager->get(request);
} else {
m_inFlightRequests.remove(cacheKey);
m_inFlightRequests.remove(requestKey);
return;
}
}
@ -906,21 +819,75 @@ void ChatBlockModel::onDiagramReply(QNetworkReply *reply)
errorMsg = reply->errorString();
}
// Update cache
if (success) {
m_diagramCache[cacheKey] = imageData;
// Keep the result only on the waiting segments; do not cache diagrams.
} else {
// Cache error as empty data with special marker
m_diagramCache[cacheKey] = QByteArray("ERROR:") + errorMsg.toUtf8();
// The error is applied directly to waiting segments below.
}
// Rebuild all blocks waiting for this diagram
// Apply the response directly to waiting segments. Nothing is retained after
// this request completes; a later occurrence renders through the local server.
const QList<int> blocks = m_pendingDiagrams.take(cacheKey);
for (int blockIndex : blocks) {
if (blockIndex < m_blocks.size()) {
rebuildBlockHtml(blockIndex);
QModelIndex idx = index(blockIndex);
emit dataChanged(idx, idx, {RenderedHtmlRole, SegmentsRole});
if (blockIndex < 0 || blockIndex >= m_blocks.size())
continue;
ChatBlock &block = m_blocks[blockIndex];
BlockSegment *matchingSegment = nullptr;
for (BlockSegment &segment : block.segments) {
if (segment.type != BlockSegment::Diagram || !segment.diagramLoading)
continue;
const QString segmentKey = segment.diagramLang + QStringLiteral(":") +
QString::fromLatin1(QCryptographicHash::hash(
segment.diagramSource.toUtf8(), QCryptographicHash::Md5).toHex());
if (segmentKey == cacheKey) {
matchingSegment = &segment;
break;
}
}
// A block can be rebuilt while a request is in flight. In that case
// the segment still represents this request, but its source may have
// been normalized by the parser. Use the sole pending diagram rather
// than leaving the UI loading forever.
if (!matchingSegment) {
for (BlockSegment &segment : block.segments) {
if (segment.type == BlockSegment::Diagram && segment.diagramLoading) {
if (matchingSegment) {
matchingSegment = nullptr;
break;
}
matchingSegment = &segment;
}
}
}
if (!matchingSegment)
continue;
BlockSegment &segment = *matchingSegment;
segment.diagramLoading = false;
if (!success) {
segment.diagramError = errorMsg;
segment.html = diagramErrorHtml(errorMsg);
} else if (imageData.startsWith("JPEG:")) {
const QByteArray data = imageData.mid(5);
segment.diagramSvg = QStringLiteral("data:image/jpeg;base64,") +
QString::fromLatin1(data.toBase64());
segment.html = diagramJpegHtml(data);
} else if (imageData.startsWith("PNG:")) {
const QByteArray data = imageData.mid(4);
segment.diagramSvg = QStringLiteral("data:image/png;base64,") +
QString::fromLatin1(data.toBase64());
segment.html = diagramPngHtml(data);
} else {
const QByteArray data = imageData.startsWith("SVG:") ? imageData.mid(4) : imageData;
segment.diagramSvg = QStringLiteral("data:image/svg+xml;base64,") +
QString::fromLatin1(data.toBase64());
segment.html = diagramSvgHtml(data);
}
rebuildRenderedHtmlFromSegments(block);
QModelIndex idx = index(blockIndex);
emit dataChanged(idx, idx, {RenderedHtmlRole, SegmentsRole});
}
}

View File

@ -95,7 +95,6 @@ public:
Q_INVOKABLE void appendData(const QString &text);
Q_INVOKABLE void reset();
Q_INVOKABLE void trimFront(int keepCount);
Q_INVOKABLE void clearDiagramCache();
signals:
void countChanged();
@ -140,8 +139,6 @@ private:
QString m_fenceLanguage;
QString m_fenceContent;
// Diagram cache: hash of (lang + code) -> SVG data
QHash<QString, QByteArray> m_diagramCache;
// Pending requests: hash -> block indices waiting for this diagram
QHash<QString, QList<int>> m_pendingDiagrams;
// Track which requests are in flight