feat: add Kimi Code and web APIs
This commit is contained in:
+176
-42
@@ -12,7 +12,9 @@
|
||||
#include <QClipboard>
|
||||
#include <QBuffer>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
#include <QPalette>
|
||||
#include <QJsonDocument>
|
||||
@@ -128,11 +130,20 @@ int countNamed(QQuickItem *root, const QString &prefix) {
|
||||
/* An item by objectName. The QObject tree is tried first (it holds the named
|
||||
* controls); a view's delegates can be owned by the model instead, so the
|
||||
* visual tree -- what a finger can actually reach -- is walked as well. */
|
||||
bool onScreen(QQuickItem *it); /* defined below: isVisible() is not enough */
|
||||
|
||||
QQuickItem *item(QQuickWindow *win, const QString &name) {
|
||||
QList<QQuickItem *> hits;
|
||||
collectNamed(win->contentItem(), name, &hits);
|
||||
/* What a finger can reach is the one the test means: a delegate on its way
|
||||
* out is still in the tree but invisible. */
|
||||
/* What a finger can reach is the one the test means. Two things say
|
||||
* "visible" without being reachable: a delegate on its way out, and a
|
||||
* second copy of a control that lives in a closed drawer (the sidebar is
|
||||
* drawn from the window and, when narrow, from a drawer off to the left).
|
||||
* Which copy comes first in the tree is not the same on every platform, so
|
||||
* the one whose centre is really on screen wins. */
|
||||
for (QQuickItem *it : hits)
|
||||
if (onScreen(it))
|
||||
return it;
|
||||
for (QQuickItem *it : hits)
|
||||
if (it->isVisible())
|
||||
return it;
|
||||
@@ -168,12 +179,66 @@ bool onScreen(QQuickItem *it) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* An event lands on whatever is at that point, and isVisible() does not know
|
||||
* about a scrolling ancestor's clipping (see onScreen): an item the view has
|
||||
* scrolled past still says it is visible, and the click goes to the row that
|
||||
* happens to be at that pixel -- which is a different row on a window whose
|
||||
* font makes the rows taller (another platform, another Qt). So a click
|
||||
* scrolls its target in first, which is what a person does. The nearest
|
||||
* ancestor that both clips and scrolls (a ListView's own item) is the one to
|
||||
* move; anchored to its middle, so a row at either edge still gets its whole
|
||||
* centre inside the viewport. */
|
||||
void scrollIntoView(QQuickItem *it, bool middle = false) {
|
||||
if (!it || (!middle && onScreen(it)))
|
||||
return;
|
||||
for (QQuickItem *a = it->parentItem(); a; a = a->parentItem()) {
|
||||
const QVariant contentY = a->property("contentY");
|
||||
if (!a->clip() || !contentY.isValid() ||
|
||||
!a->property("contentHeight").isValid())
|
||||
continue;
|
||||
const double y = it->mapToItem(a, QPointF(0, 0)).y() + contentY.toDouble();
|
||||
const double lowest = a->property("contentHeight").toDouble() - a->height();
|
||||
note(QStringLiteral("%1 is out of the viewport (contentY %2, at %3 of %4): "
|
||||
"scrolling to it")
|
||||
.arg(it->objectName())
|
||||
.arg(contentY.toDouble())
|
||||
.arg(y)
|
||||
.arg(a->property("contentHeight").toDouble()));
|
||||
a->setProperty("contentY", qBound(0.0, y - a->height() / 2, lowest));
|
||||
QTest::qWait(150);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* One rendered frame: items are laid out, and a view's delegates are created,
|
||||
* in the render loop's polish step. A platform whose window is never exposed
|
||||
* -- offscreen on Windows, which is how this is run on a machine that has no
|
||||
* Windows -- only runs that step when something asks for a picture, so
|
||||
* without this the window can be a whole pass behind: a row still 0 wide, a
|
||||
* card not created yet. A person looking at the window would see it settled,
|
||||
* which is what the checks below are about. */
|
||||
void frame(QQuickWindow *win) {
|
||||
if (win)
|
||||
win->grabWindow();
|
||||
QTest::qWait(50);
|
||||
}
|
||||
|
||||
void clickItem(QQuickWindow *win, QQuickItem *it); /* defined below */
|
||||
void tapItem(QQuickWindow *win, QPointingDevice *dev, QQuickItem *it);
|
||||
|
||||
void clickMouse(QQuickWindow *win, const QString &name) {
|
||||
QQuickItem *it = item(win, name);
|
||||
if (!it || !it->isVisible()) {
|
||||
check(false, QStringLiteral("mouse click %1: not there").arg(name));
|
||||
return;
|
||||
}
|
||||
clickItem(win, it);
|
||||
}
|
||||
|
||||
/* The same click, on an item the caller already has: a name can match more
|
||||
* than one item (a view keeps a delegate from the session before), and the
|
||||
* one the test means is not always the first in the tree. */
|
||||
void clickItem(QQuickWindow *win, QQuickItem *it) {
|
||||
QTest::mouseClick(win, Qt::LeftButton, Qt::NoModifier, centre(it));
|
||||
}
|
||||
|
||||
@@ -183,6 +248,10 @@ void tapTouch(QQuickWindow *win, QPointingDevice *dev, const QString &name) {
|
||||
check(false, QStringLiteral("touch tap %1: not there").arg(name));
|
||||
return;
|
||||
}
|
||||
tapItem(win, dev, it);
|
||||
}
|
||||
|
||||
void tapItem(QQuickWindow *win, QPointingDevice *dev, QQuickItem *it) {
|
||||
const QPoint p = centre(it);
|
||||
QTest::touchEvent(win, dev).press(0, p, win);
|
||||
QTest::touchEvent(win, dev).release(0, p, win);
|
||||
@@ -417,6 +486,10 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
QStringLiteral("a session fixture with a tool call is in the store"));
|
||||
QMetaObject::invokeMethod(win, "reloadSessions");
|
||||
QTest::qWait(150);
|
||||
/* A row is not clickable until the layout pass that gives it its width has
|
||||
* run (see frame()): a click on a row that is still 0 wide goes to whatever
|
||||
* is behind it. */
|
||||
frame(win);
|
||||
int rowsSeen = countNamed(win->contentItem(), QStringLiteral("sessionRow:"));
|
||||
if (qEnvironmentVariableIsSet("CST_APP_DUMP"))
|
||||
std::printf("note: %d session rows in the visual tree\n", rowsSeen);
|
||||
@@ -465,61 +538,89 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
/* A click goes to whatever is at that point, and the transcript replays
|
||||
* from its bottom: on a phone-sized window the card can be above the
|
||||
* visible part, where isVisible() still says yes (it does not know about
|
||||
* the view's clipping) and the click lands on the row that happens to be
|
||||
* there. Scroll it in first, which is what a person does -- and stop the
|
||||
* view following its tail (ChatView.stickBottom), or the scroll is undone
|
||||
* by the next content change. */
|
||||
if (list && !onScreen(item(win, QStringLiteral("fileOpen0")))) {
|
||||
QQuickItem *button = item(win, QStringLiteral("fileOpen0"));
|
||||
QQuickItem *chat = item(win, QStringLiteral("chatView"));
|
||||
if (chat)
|
||||
* the view's clipping). scrollIntoView puts it in the middle of the view --
|
||||
* and stop the view following its tail (ChatView.stickBottom) first, or
|
||||
* the scroll is undone by the next content change. */
|
||||
if (list) {
|
||||
/* Stop the view following its tail (ChatView.stickBottom) first: the
|
||||
* scroll to the card below would be undone by the next content
|
||||
* change. */
|
||||
if (QQuickItem *chat = item(win, QStringLiteral("chatView")))
|
||||
chat->setProperty("stickBottom", false);
|
||||
const double contentY = list->property("contentY").toDouble();
|
||||
const double y = button->mapToItem(list, QPointF(0, 0)).y() + contentY;
|
||||
const double lowest =
|
||||
list->property("contentHeight").toDouble() - list->height();
|
||||
note(QStringLiteral("the card is out of the viewport (contentY %1, card "
|
||||
"at %2 of %3): scrolling to it")
|
||||
.arg(contentY)
|
||||
.arg(y)
|
||||
.arg(list->property("contentHeight").toDouble()));
|
||||
list->setProperty("contentY", qBound(0.0, y - list->height() / 2, lowest));
|
||||
QTest::qWait(150);
|
||||
}
|
||||
clickMouse(win, QStringLiteral("fileOpen0"));
|
||||
/* What the card itself hands the engine ("rel" is the name the store keeps
|
||||
* the file under), and whether it is really on screen: a card outside the
|
||||
* viewport takes the events meant for it, and the card's own line is where
|
||||
* the engine puts its refusal. */
|
||||
QStringList rels;
|
||||
QString cardSays;
|
||||
QList<QQuickItem *> strips;
|
||||
collectNamed(win->contentItem(), QStringLiteral("imageStrip"), &strips);
|
||||
/* Which card is the session's own. The tree still holds the delegate of
|
||||
* the session before -- a view keeps a row or two around -- and its card
|
||||
* names a file of *that* session, so the first "fileOpen0" in the tree is
|
||||
* not necessarily the one the check means. A rel this session can open is
|
||||
* one that is really in its attachment directory. */
|
||||
QQuickItem *openButton = nullptr;
|
||||
QQuickItem *shareButton = nullptr;
|
||||
const QString attachDir = core.attachmentDir();
|
||||
for (QQuickItem *s : strips) {
|
||||
const QVariantList files = s->property("files").toList();
|
||||
for (const QVariant &f : files)
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
const QVariant &f = files.at(i);
|
||||
rels << f.toMap().value(QStringLiteral("rel")).toString();
|
||||
if (openButton) {
|
||||
continue;
|
||||
}
|
||||
const QString rel =
|
||||
f.toMap().value(QStringLiteral("rel")).toString();
|
||||
if (rel.isEmpty() ||
|
||||
!QFileInfo::exists(attachDir + QLatin1Char('/') + rel))
|
||||
continue;
|
||||
QList<QQuickItem *> hits;
|
||||
collectNamed(s, QStringLiteral("fileOpen%1").arg(i), &hits);
|
||||
if (!hits.isEmpty()) {
|
||||
openButton = hits.first();
|
||||
collectNamed(s, QStringLiteral("fileShare%1").arg(i), &hits);
|
||||
shareButton = hits.isEmpty() ? nullptr : hits.first();
|
||||
}
|
||||
}
|
||||
if (cardSays.isEmpty())
|
||||
cardSays = s->property("errorText").toString();
|
||||
}
|
||||
if (!openButton)
|
||||
openButton = item(win, QStringLiteral("fileOpen0"));
|
||||
if (!shareButton)
|
||||
shareButton = item(win, QStringLiteral("fileShare0"));
|
||||
/* What the card itself hands the engine ("rel" is the name the store keeps
|
||||
* the file under), and whether it is really on screen: a card outside the
|
||||
* viewport takes the events meant for it, and the card's own line is where
|
||||
* the engine puts its refusal. */
|
||||
if (openButton) {
|
||||
scrollIntoView(openButton, true);
|
||||
frame(win);
|
||||
clickItem(win, openButton);
|
||||
} else {
|
||||
check(false, QStringLiteral("the delivered file has a card to open"));
|
||||
}
|
||||
const bool byMouse = !win->property("statusText").toString().isEmpty();
|
||||
if (!byMouse)
|
||||
tapTouch(win, touch, QStringLiteral("fileOpen0"));
|
||||
if (!byMouse && openButton)
|
||||
tapItem(win, touch, openButton);
|
||||
const QString opened = win->property("statusText").toString();
|
||||
check(opened.startsWith(QLatin1String("opened ")),
|
||||
QStringLiteral("Open hands the file to the system (%1%2; cards hold '%3', "
|
||||
"%4, card says '%5')")
|
||||
.arg(opened, byMouse ? QString() : QStringLiteral(" after a tap"),
|
||||
rels.join(QLatin1String(", ")),
|
||||
onScreen(item(win, QStringLiteral("fileOpen0")))
|
||||
onScreen(openButton)
|
||||
? QStringLiteral("on screen")
|
||||
: QStringLiteral("out of the viewport"),
|
||||
cardSays));
|
||||
win->setProperty("statusText", QString());
|
||||
clickMouse(win, QStringLiteral("fileShare0"));
|
||||
if (shareButton) {
|
||||
scrollIntoView(shareButton, true);
|
||||
frame(win);
|
||||
clickItem(win, shareButton);
|
||||
}
|
||||
const bool shareByMouse = !win->property("statusText").toString().isEmpty();
|
||||
if (!shareByMouse)
|
||||
tapTouch(win, touch, QStringLiteral("fileShare0"));
|
||||
if (!shareByMouse && shareButton)
|
||||
tapItem(win, touch, shareButton);
|
||||
const QString shared = win->property("statusText").toString();
|
||||
check(shared.contains(QLatin1String("report.md")),
|
||||
QStringLiteral("Share names the file (%1%2; card says '%3')")
|
||||
@@ -709,27 +810,46 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
|
||||
/* and back to the fixture, which still has its six rows */
|
||||
QTest::qWait(150);
|
||||
/* The sidebar holds two more sessions by now, so the row may sit below the
|
||||
* fold: brought into view first, or the click goes to the row that happens
|
||||
* to be there (see scrollIntoView). */
|
||||
if (QQuickItem *row = item(win, QStringLiteral("sessionRow:") + fixture)) {
|
||||
scrollIntoView(row);
|
||||
frame(win);
|
||||
}
|
||||
clickMouse(win, QStringLiteral("sessionRow:") + fixture);
|
||||
check(core.sessionId() == fixture && list &&
|
||||
list->property("count").toInt() == 6,
|
||||
QStringLiteral("switching back replays it again"));
|
||||
{
|
||||
bool promptImage = false;
|
||||
int promptImageRow = -1;
|
||||
const QVariantList rows = core.history();
|
||||
for (const QVariant &v : rows) {
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
const QVariant &v = rows.at(i);
|
||||
const QVariantMap r = v.toMap();
|
||||
if (r.value(QStringLiteral("role")).toString() == QLatin1String("user") &&
|
||||
r.value(QStringLiteral("images")).toString().contains(
|
||||
QLatin1String("|")))
|
||||
QLatin1String("|"))) {
|
||||
promptImage = true;
|
||||
if (promptImageRow < 0)
|
||||
promptImageRow = i;
|
||||
}
|
||||
}
|
||||
check(promptImage,
|
||||
QStringLiteral("a stored prompt image comes back with its row"));
|
||||
/* The view follows the tail (ChatView.stickBottom) and the picture is in
|
||||
* the first prompt: on a window that fits fewer rows than this one it is
|
||||
* above the visible part, where a delegate is not created at all. Put it
|
||||
* on screen, which is what a person does before looking at it. */
|
||||
if (list && promptImageRow >= 0)
|
||||
QMetaObject::invokeMethod(list, "positionViewAtIndex",
|
||||
Q_ARG(int, promptImageRow), Q_ARG(int, 0));
|
||||
/* The rows are in the model by now; the cards -- and the thumbnails in
|
||||
* them -- are built on the view's next layout pass. A frame forces it,
|
||||
* and that frame is also the picture a person looks at. */
|
||||
maybeShot(win, QStringLiteral("-images"));
|
||||
QTest::qWait(150);
|
||||
frame(win);
|
||||
if (qEnvironmentVariableIsSet("CST_APP_DUMP")) {
|
||||
QVariant v;
|
||||
QMetaObject::invokeMethod(win, "rowJson", Q_RETURN_ARG(QVariant, v),
|
||||
@@ -1057,9 +1177,13 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
/* --- the image provider: only the session's own files ----------------- */
|
||||
{
|
||||
const QString dir = core.attachmentDir();
|
||||
/* one directory per session since v0.9.5: <sessions>/<id>/attachments */
|
||||
check(!dir.isEmpty() && dir.endsWith(QLatin1String("/attachments")) &&
|
||||
dir.contains(QLatin1String("/sessions/")),
|
||||
/* one directory per session since v0.9.5: <sessions>/<id>/attachments.
|
||||
* The engine builds the path with the separator of the platform it runs
|
||||
* on, so what arrives here is "…/sessions\<id>\attachments" on Windows:
|
||||
* asked with forward slashes, which is what the check means. */
|
||||
const QString shown = QDir::fromNativeSeparators(dir);
|
||||
check(!shown.isEmpty() && shown.endsWith(QLatin1String("/attachments")) &&
|
||||
shown.contains(QLatin1String("/sessions/")),
|
||||
QStringLiteral("the agent names its image directory (%1)").arg(dir));
|
||||
QImage pic(32, 16, QImage::Format_RGB32);
|
||||
pic.fill(Qt::darkGreen);
|
||||
@@ -1100,6 +1224,14 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
const int before = list->property("count").toInt();
|
||||
const int thumbsBefore =
|
||||
countNamed(win->contentItem(), QStringLiteral("imageThumb"));
|
||||
if (qEnvironmentVariableIsSet("CST_APP_DUMP") && list)
|
||||
std::printf("note: tool card: model %d, contentY %.1f of %.1f (view %.1f), "
|
||||
"%d strips, %d thumbs, %d cards\n", before,
|
||||
list->property("contentY").toDouble(),
|
||||
list->property("contentHeight").toDouble(), list->height(),
|
||||
countNamed(win->contentItem(), QStringLiteral("imageStrip")),
|
||||
thumbsBefore,
|
||||
countNamed(win->contentItem(), QStringLiteral("card:")));
|
||||
event4("engineToolEvent", QStringLiteral("open"), QStringLiteral("call-img"),
|
||||
QStringLiteral("view_image"), QStringLiteral("{\"path\":\"shot.png\"}"));
|
||||
const QString imgs =
|
||||
@@ -1108,7 +1240,7 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
Q_ARG(QVariant, QStringLiteral("call-img")),
|
||||
Q_ARG(QVariant, imgs)),
|
||||
QStringLiteral("images a tool made reach the view"));
|
||||
QTest::qWait(150);
|
||||
frame(win);
|
||||
const QVariantMap card = rowAt(before);
|
||||
check(card.value(QStringLiteral("images"))
|
||||
.toString()
|
||||
@@ -1118,7 +1250,9 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
QStringLiteral("the card of that call carries the image"));
|
||||
check(countNamed(win->contentItem(), QStringLiteral("imageThumb")) >
|
||||
thumbsBefore,
|
||||
QStringLiteral("its thumbnail is in the card"));
|
||||
QStringLiteral("its thumbnail is in the card (%1 -> %2)").arg(thumbsBefore)
|
||||
.arg(countNamed(win->contentItem(),
|
||||
QStringLiteral("imageThumb"))));
|
||||
maybeShot(win, QStringLiteral("-toolimage"));
|
||||
}
|
||||
|
||||
@@ -1144,7 +1278,7 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
Q_ARG(QVariant, QStringLiteral("call-file")),
|
||||
Q_ARG(QVariant, files)),
|
||||
QStringLiteral("the files a tool delivered reach the view"));
|
||||
QTest::qWait(150);
|
||||
frame(win);
|
||||
const QVariantMap card = rowAt(before);
|
||||
check(card.value(QStringLiteral("images"))
|
||||
.toString()
|
||||
|
||||
Reference in New Issue
Block a user