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.
|
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
|
```text
|
||||||
frontends (shell, Acme, KDE, scripts)
|
frontends (shell, Acme, KDE, scripts)
|
||||||
│ 9P
|
│ 9P
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,6 @@ void ChatBlockModel::reset()
|
||||||
m_fenceContent.clear();
|
m_fenceContent.clear();
|
||||||
m_pendingDiagrams.clear();
|
m_pendingDiagrams.clear();
|
||||||
m_inFlightRequests.clear();
|
m_inFlightRequests.clear();
|
||||||
m_diagramCache.clear(); // Clear cache on reset to pick up fixes
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
emit countChanged();
|
emit countChanged();
|
||||||
}
|
}
|
||||||
|
|
@ -258,12 +257,6 @@ void ChatBlockModel::trimFront(int keepCount)
|
||||||
emit countChanged();
|
emit countChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatBlockModel::clearDiagramCache()
|
|
||||||
{
|
|
||||||
m_diagramCache.clear();
|
|
||||||
qDebug() << "Diagram cache cleared";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatBlockModel::getDefaultTextColor() const
|
QString ChatBlockModel::getDefaultTextColor() const
|
||||||
{
|
{
|
||||||
const auto textColor = QApplication::palette().color(QPalette::Text);
|
const auto textColor = QApplication::palette().color(QPalette::Text);
|
||||||
|
|
@ -375,57 +368,11 @@ void ChatBlockModel::rebuildBlockHtml(int blockIndex)
|
||||||
html += diagramPlaceholder(fenceLang);
|
html += diagramPlaceholder(fenceLang);
|
||||||
seg.diagramLoading = true;
|
seg.diagramLoading = true;
|
||||||
} else {
|
} else {
|
||||||
// Complete fence — check cache or request diagram
|
// Complete fence — render through the local diagram server.
|
||||||
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);
|
html += diagramPlaceholder(fenceLang);
|
||||||
seg.diagramLoading = true;
|
seg.diagramLoading = true;
|
||||||
// Request diagram asynchronously
|
|
||||||
requestDiagram(fenceLang, code, blockIndex);
|
requestDiagram(fenceLang, code, blockIndex);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
segments.append(seg);
|
segments.append(seg);
|
||||||
} else {
|
} else {
|
||||||
QString codeHtml = highlightCode(code, fenceLang);
|
QString codeHtml = highlightCode(code, fenceLang);
|
||||||
|
|
@ -575,33 +522,8 @@ void ChatBlockModel::incrementalAppend(int blockIndex, const QString &line)
|
||||||
seg.diagramLang = rs.fenceLang;
|
seg.diagramLang = rs.fenceLang;
|
||||||
seg.diagramSource = code;
|
seg.diagramSource = code;
|
||||||
seg.html = highlightCode(code, rs.fenceLang);
|
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 {
|
|
||||||
seg.diagramLoading = true;
|
seg.diagramLoading = true;
|
||||||
requestDiagram(rs.fenceLang, code, blockIndex);
|
requestDiagram(rs.fenceLang, code, blockIndex);
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the in-progress fence segment
|
// Replace the in-progress fence segment
|
||||||
if (!block.segments.isEmpty())
|
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)
|
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(
|
QString::fromLatin1(QCryptographicHash::hash(
|
||||||
code.toUtf8(), QCryptographicHash::Md5).toHex());
|
code.toUtf8(), QCryptographicHash::Md5).toHex());
|
||||||
|
|
||||||
// Already have it cached?
|
// Track duplicate requests only while this local render is in flight.
|
||||||
if (m_diagramCache.contains(cacheKey))
|
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;
|
return;
|
||||||
|
|
||||||
// Track which blocks need this diagram
|
m_inFlightRequests.insert(requestKey);
|
||||||
if (!m_pendingDiagrams.contains(cacheKey)) {
|
|
||||||
m_pendingDiagrams[cacheKey] = QList<int>();
|
|
||||||
}
|
|
||||||
if (!m_pendingDiagrams[cacheKey].contains(blockIndex)) {
|
|
||||||
m_pendingDiagrams[cacheKey].append(blockIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Already requesting?
|
|
||||||
if (m_inFlightRequests.contains(cacheKey))
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_inFlightRequests.insert(cacheKey);
|
|
||||||
|
|
||||||
// Build request
|
|
||||||
QNetworkRequest request;
|
QNetworkRequest request;
|
||||||
request.setAttribute(QNetworkRequest::User, cacheKey);
|
request.setAttribute(QNetworkRequest::User, requestKey);
|
||||||
request.setTransferTimeout(10000);
|
request.setTransferTimeout(10000);
|
||||||
|
|
||||||
const QString langLower = lang.toLower();
|
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)));
|
request.setUrl(QUrl(QStringLiteral("http://localhost:8081/img/") + QString::fromLatin1(encoded)));
|
||||||
m_netManager->get(request);
|
m_netManager->get(request);
|
||||||
} else {
|
} else {
|
||||||
m_inFlightRequests.remove(cacheKey);
|
m_inFlightRequests.remove(requestKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -906,21 +819,75 @@ void ChatBlockModel::onDiagramReply(QNetworkReply *reply)
|
||||||
errorMsg = reply->errorString();
|
errorMsg = reply->errorString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cache
|
|
||||||
if (success) {
|
if (success) {
|
||||||
m_diagramCache[cacheKey] = imageData;
|
// Keep the result only on the waiting segments; do not cache diagrams.
|
||||||
} else {
|
} else {
|
||||||
// Cache error as empty data with special marker
|
// The error is applied directly to waiting segments below.
|
||||||
m_diagramCache[cacheKey] = QByteArray("ERROR:") + errorMsg.toUtf8();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
const QList<int> blocks = m_pendingDiagrams.take(cacheKey);
|
||||||
for (int blockIndex : blocks) {
|
for (int blockIndex : blocks) {
|
||||||
if (blockIndex < m_blocks.size()) {
|
if (blockIndex < 0 || blockIndex >= m_blocks.size())
|
||||||
rebuildBlockHtml(blockIndex);
|
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);
|
QModelIndex idx = index(blockIndex);
|
||||||
emit dataChanged(idx, idx, {RenderedHtmlRole, SegmentsRole});
|
emit dataChanged(idx, idx, {RenderedHtmlRole, SegmentsRole});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,6 @@ public:
|
||||||
Q_INVOKABLE void appendData(const QString &text);
|
Q_INVOKABLE void appendData(const QString &text);
|
||||||
Q_INVOKABLE void reset();
|
Q_INVOKABLE void reset();
|
||||||
Q_INVOKABLE void trimFront(int keepCount);
|
Q_INVOKABLE void trimFront(int keepCount);
|
||||||
Q_INVOKABLE void clearDiagramCache();
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void countChanged();
|
void countChanged();
|
||||||
|
|
@ -140,8 +139,6 @@ private:
|
||||||
QString m_fenceLanguage;
|
QString m_fenceLanguage;
|
||||||
QString m_fenceContent;
|
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
|
// Pending requests: hash -> block indices waiting for this diagram
|
||||||
QHash<QString, QList<int>> m_pendingDiagrams;
|
QHash<QString, QList<int>> m_pendingDiagrams;
|
||||||
// Track which requests are in flight
|
// Track which requests are in flight
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue