From 6819b5f391f120698cee9db50a3f9d570848886c Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Tue, 18 Aug 2026 12:11:04 +0200 Subject: [PATCH] Simplify diagram rendering and document architecture --- README.md | 41 ++++++++ kde/gui/chatblockmodel.cpp | 201 ++++++++++++++++--------------------- kde/gui/chatblockmodel.h | 3 - 3 files changed, 125 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index b0b66a0..d926bf3 100644 --- a/README.md +++ b/README.md @@ -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
9P namespace · session lifecycle · agent runtime
prompt + history · backend dispatch"] + + S1["Session A (toolsrv)
host + cwd"] + S2["Session B (toolsrv)
host + cwd"] + S3["Session C (toolsrv)
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 diff --git a/kde/gui/chatblockmodel.cpp b/kde/gui/chatblockmodel.cpp index 7f78e12..927e713 100644 --- a/kde/gui/chatblockmodel.cpp +++ b/kde/gui/chatblockmodel.cpp @@ -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(); + 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(); - } - 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 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}); } } diff --git a/kde/gui/chatblockmodel.h b/kde/gui/chatblockmodel.h index 6a795e4..da75296 100644 --- a/kde/gui/chatblockmodel.h +++ b/kde/gui/chatblockmodel.h @@ -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 m_diagramCache; // Pending requests: hash -> block indices waiting for this diagram QHash> m_pendingDiagrams; // Track which requests are in flight