v0.8.13: tool cards paired by call id, markdown rendered while it streams
Two things the window got wrong once a real turn ran through it, plus the switch that was invisible on a dark theme. Tool cards (app/src/core/AgentCore.*, app/qml/Main.qml, ToolCard.qml): - A turn that called the same tool twice left the first card on "running" forever and gave its result to the second card: both were paired by name. cst_agent_hooks_t hands the call (ctl_tool_call_t) to every callback, so the id travels with the signals now (toolEvent(phase, id, name, detail), toolResult(id, name, exitCode, output)) and findTool() matches on it, in the same way AgentCore::history() fills it from tool_call_id / tool_calls_json for a replayed session. Only a call without an id -- an old text-protocol session -- still falls back to the name. - Nothing may keep spinning after a turn: closeAllRows() runs when the turn ends and settles every open row; a call that never came back shows "no result" (state "unfinished") and no longer claims an exit code. Streaming text (app/qml/Main.qml, ChatView.qml): - The answer was plain text while it streamed and turned into markdown only when the turn ended. A 120 ms timer now re-renders the open answer with the engine's own renderer (AgentCore::renderMarkdown -> cst_web_md_html), so a list, a table or a code fence fills in formatted as it arrives. A row keeps its own "html" field; a replayed session renders once, where its rows are built. QML details: - the Plan mode Switch got a drawn indicator: the Basic style's pill is a dark pill with a dark knob on this theme, which made the control look disabled. - Every engine signal handler is a plain window function now (engineToolEvent, engineToolResult, engineTurnFinished, engineAssistant, ...), which is also what lets --uitest drive the exact path; rowJson(i) returns a transcript row as JSON for it (an object would not survive QMetaObject::invokeMethod). Tests: 6 more checks -- a call opens a card, two calls of one tool are two cards, the result lands on the card of its own call, the other card is untouched, the turn end settles an open call, a delta opens a streaming answer and markdown is rendered while it still streams. 52/52 pass, --selftest, ctest cst-app-selftest. Version 0.8.13.
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
#include <QClipboard>
|
||||
#include <QFile>
|
||||
#include <QImage>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QPointingDevice>
|
||||
#include <QQuickItem>
|
||||
#include <QQmlApplicationEngine>
|
||||
@@ -372,6 +374,82 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
check(false, QStringLiteral("there is a clipboard to paste from"));
|
||||
}
|
||||
|
||||
/* --- tool calls: two calls of one tool are two cards ------------------ */
|
||||
const int rows0 = list ? list->property("count").toInt() : -1;
|
||||
auto rowAt = [&](int i) {
|
||||
QVariant v;
|
||||
QVariantMap r;
|
||||
if (!QMetaObject::invokeMethod(win, "rowJson", Q_RETURN_ARG(QVariant, v),
|
||||
Q_ARG(QVariant, i)))
|
||||
return r;
|
||||
return QJsonDocument::fromJson(v.toString().toUtf8()).object().toVariantMap();
|
||||
};
|
||||
auto event4 = [&](const char *fn, const QVariant &a, const QVariant &b,
|
||||
const QVariant &c, const QVariant &d) {
|
||||
return QMetaObject::invokeMethod(win, fn, Q_ARG(QVariant, a),
|
||||
Q_ARG(QVariant, b), Q_ARG(QVariant, c),
|
||||
Q_ARG(QVariant, d));
|
||||
};
|
||||
check(event4("engineToolEvent", QStringLiteral("open"), QStringLiteral("call-1"),
|
||||
QStringLiteral("list_dir"), QStringLiteral("{\"path\":\"vendor\"}")) &&
|
||||
list->property("count").toInt() == rows0 + 1,
|
||||
QStringLiteral("a tool call opens a card"));
|
||||
event4("engineToolEvent", QStringLiteral("run"), QStringLiteral("call-1"),
|
||||
QStringLiteral("list_dir"), QString());
|
||||
/* the same tool a second time, while the first call is still open: pairing
|
||||
by name is what left the first card on "running" forever */
|
||||
event4("engineToolEvent", QStringLiteral("open"), QStringLiteral("call-2"),
|
||||
QStringLiteral("list_dir"), QStringLiteral("{\"path\":\"src\"}"));
|
||||
event4("engineToolResult", QStringLiteral("call-1"), QStringLiteral("list_dir"),
|
||||
QVariant(0), QStringLiteral("exit=0 bytes=532"));
|
||||
check(list->property("count").toInt() == rows0 + 2,
|
||||
QStringLiteral("two calls of one tool are two cards (%1 -> %2)")
|
||||
.arg(rows0)
|
||||
.arg(list->property("count").toInt()));
|
||||
{
|
||||
const QVariantMap first = rowAt(rows0), second = rowAt(rows0 + 1);
|
||||
check(first.value(QStringLiteral("state")).toString() == QLatin1String("done") &&
|
||||
first.value(QStringLiteral("output")).toString() ==
|
||||
QLatin1String("exit=0 bytes=532"),
|
||||
QStringLiteral("the result lands on the card of its own call "
|
||||
"(state '%1', output '%2', keys [%3])")
|
||||
.arg(first.value(QStringLiteral("state")).toString(),
|
||||
first.value(QStringLiteral("output")).toString(),
|
||||
first.keys().join(QLatin1Char(','))));
|
||||
check(second.value(QStringLiteral("state")).toString() == QLatin1String("pending") &&
|
||||
second.value(QStringLiteral("detail")).toString().contains(
|
||||
QLatin1String("\"src\"")) &&
|
||||
second.value(QStringLiteral("output")).toString().isEmpty(),
|
||||
QStringLiteral("the other card is untouched (state '%1')")
|
||||
.arg(second.value(QStringLiteral("state")).toString()));
|
||||
}
|
||||
/* and a turn that ends with the call still open settles it */
|
||||
check(QMetaObject::invokeMethod(win, "engineTurnFinished", Q_ARG(QVariant, 0),
|
||||
Q_ARG(QVariant, QString())),
|
||||
QStringLiteral("the turn end is reachable too"));
|
||||
{
|
||||
const QVariantMap second = rowAt(rows0 + 1);
|
||||
check(second.value(QStringLiteral("complete")).toBool() &&
|
||||
second.value(QStringLiteral("state")).toString() ==
|
||||
QLatin1String("unfinished"),
|
||||
QStringLiteral("an open call is settled when the turn ends"));
|
||||
}
|
||||
|
||||
/* --- the answer is rendered while it streams -------------------------- */
|
||||
if (QMetaObject::invokeMethod(win, "engineAssistant", Q_ARG(QVariant, QStringLiteral("**bold** text")))) {
|
||||
const int last = list->property("count").toInt() - 1;
|
||||
QMetaObject::invokeMethod(win, "renderStreaming");
|
||||
const QVariantMap row = rowAt(last);
|
||||
check(row.value(QStringLiteral("kind")).toString() == QLatin1String("message") &&
|
||||
!row.value(QStringLiteral("complete")).toBool(),
|
||||
QStringLiteral("a delta opens a streaming answer"));
|
||||
check(row.value(QStringLiteral("html")).toString().contains(QLatin1Char('<')) &&
|
||||
row.value(QStringLiteral("text")).toString().contains(QLatin1String("bold")),
|
||||
QStringLiteral("markdown is rendered while the answer streams"));
|
||||
} else {
|
||||
check(false, QStringLiteral("assistant deltas are reachable"));
|
||||
}
|
||||
|
||||
/* --- a narrow window: dock -> drawer, and the button opens it --- */
|
||||
resizeTo(win, 460, 900);
|
||||
check(docked && !docked->isVisible(),
|
||||
|
||||
Reference in New Issue
Block a user