Simplify diagram rendering and document architecture
This commit is contained in:
parent
5069755aaa
commit
6819b5f391
41
README.md
41
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<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
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue