Skip to content

Commit 740233f

Browse files
feat(mini-app): resolve CDP page ID before sending to AI
- Fetching the page list upfront lets the AI skip the discovery step and target the exact page ID directly, avoiding races where a new page could be created instead - Appending the current URL gives the AI immediate context without an extra CDP round-trip - 500 ms transfer timeout keeps the synchronous event-loop fetch from blocking the UI noticeably on a local CDP port - The old prompt forced the AI to discover the page ID itself via /json/list on every session, wasting a round-trip and risking it targeting the wrong target if multiple pages existed. - Resolving the page ID eagerly at "Send to AI" time lets us embed the exact target ID in the prompt, eliminating ambiguity and removing one tool call from the AI's hot path. - Current URL is surfaced via a new currentUrl() accessor so the prompt can include navigation context without the AI needing to query it. - DoubleShiftFilter removed as part of unrelated cleanup in the same area; the quick-open shortcut is handled elsewhere. - QuickFileOpenDialog gains a fuzzyMatch variant that captures match positions alongside the score, enabling the new HighlightDelegate to render matched characters in a distinct color without a second scan. - Improve-prompt button reparented from viewport to the input widget itself so it stays anchored during scroll and raise() ensures it stays above newly painted content.
1 parent 1943c0b commit 740233f

6 files changed

Lines changed: 229 additions & 60 deletions

File tree

src/MiniAppManager.cpp

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@
1919
#include <QDesktopServices>
2020
#include <QDialog>
2121
#include <QDir>
22+
#include <QEventLoop>
2223
#include <QFont>
2324
#include <QHostAddress>
25+
#include <QJsonArray>
26+
#include <QJsonDocument>
27+
#include <QJsonObject>
2428
#include <QMenu>
2529
#include <QMessageBox>
30+
#include <QNetworkAccessManager>
31+
#include <QNetworkReply>
2632
#include <QPainter>
2733
#include <QPlainTextEdit>
2834
#include <QProcess>
@@ -54,6 +60,30 @@ static QIcon tintIcon(const QString &svgPath, const QColor &color)
5460
return dst;
5561
}
5662

63+
static QString fetchCdpPageId(const QString &cdpHttpUrl)
64+
{
65+
QUrl listUrl(cdpHttpUrl + QStringLiteral("/json/list"));
66+
QNetworkAccessManager nam;
67+
QNetworkRequest req(listUrl);
68+
req.setTransferTimeout(500);
69+
QNetworkReply *reply = nam.get(req);
70+
QEventLoop loop;
71+
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
72+
loop.exec();
73+
if (reply->error() != QNetworkReply::NoError) {
74+
reply->deleteLater();
75+
return {};
76+
}
77+
const QJsonArray arr = QJsonDocument::fromJson(reply->readAll()).array();
78+
reply->deleteLater();
79+
for (const QJsonValue &v : arr) {
80+
const QJsonObject obj = v.toObject();
81+
if (obj.value(QStringLiteral("type")).toString() == QStringLiteral("page"))
82+
return obj.value(QStringLiteral("id")).toString();
83+
}
84+
return {};
85+
}
86+
5787
MiniAppManager::MiniAppManager(NotepadNextApplication *app,
5888
MiniAppRegistry *registry,
5989
DockedEditor *dockedEditor,
@@ -159,10 +189,22 @@ void MiniAppManager::launchApp(const MiniAppDefinition &def)
159189
AiAgentDock *aiDock = mainWin ? mainWin->activeAiDock() : nullptr;
160190
if (aiDock && !instance->cdpHttpUrl().isEmpty()) {
161191
menu.addAction(tr("Send to AI"), this, [instance, aiDock]() {
192+
const QString cdpUrl = instance->cdpHttpUrl();
193+
QString currentPage;
194+
if (auto *wv = instance->webViewWidget()) {
195+
const QString url = wv->currentUrl();
196+
if (!url.isEmpty())
197+
currentPage = QStringLiteral(" Currently on: %1.").arg(url);
198+
}
199+
const QString pageId = fetchCdpPageId(cdpUrl);
200+
QString pageConstraint;
201+
if (!pageId.isEmpty())
202+
pageConstraint = QStringLiteral(" Use only target/page ID %1 — do not create or switch to other pages.").arg(pageId);
203+
else
204+
pageConstraint = QStringLiteral(" Do not create or switch to other pages.");
162205
const QString msg = QStringLiteral(
163-
"{{ Connect to the browser via CDP at %1 "
164-
"(list available pages first, then interact only with the existing page — do not create new pages). }}\n\n")
165-
.arg(instance->cdpHttpUrl());
206+
"--connect %1 (via CDP).%2%3\n\n")
207+
.arg(cdpUrl, currentPage, pageConstraint);
166208
aiDock->insertTextToInput(msg);
167209
aiDock->setVisible(true);
168210
aiDock->raise();
@@ -359,10 +401,20 @@ void MiniAppManager::launchQuickBrowser(const QUrl &url, bool enableCdp,
359401
AiAgentDock *aiDock = mainWin ? mainWin->activeAiDock() : nullptr;
360402
if (aiDock && !webView->cdpHttpUrl().isEmpty()) {
361403
menu.addAction(tr("Send to AI"), this, [webView, aiDock]() {
404+
const QString cdpUrl = webView->cdpHttpUrl();
405+
QString currentPage;
406+
const QString url = webView->currentUrl();
407+
if (!url.isEmpty())
408+
currentPage = QStringLiteral(" Currently on: %1.").arg(url);
409+
const QString pageId = fetchCdpPageId(cdpUrl);
410+
QString pageConstraint;
411+
if (!pageId.isEmpty())
412+
pageConstraint = QStringLiteral(" Use only target/page ID %1 — do not create or switch to other pages.").arg(pageId);
413+
else
414+
pageConstraint = QStringLiteral(" Do not create or switch to other pages.");
362415
const QString msg = QStringLiteral(
363-
"{{ Connect to the browser via CDP at %1 "
364-
"(list available pages first, then interact only with the existing page — do not create new pages). }}\n\n")
365-
.arg(webView->cdpHttpUrl());
416+
"--connect %1 (via CDP).%2%3\n\n")
417+
.arg(cdpUrl, currentPage, pageConstraint);
366418
aiDock->insertTextToInput(msg);
367419
aiDock->setVisible(true);
368420
aiDock->raise();

src/dialogs/MainWindow.cpp

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -229,42 +229,6 @@ bool forwardClipboardToFocusWidget(const char *slot)
229229
return QMetaObject::invokeMethod(w, slot);
230230
}
231231

232-
class DoubleShiftFilter : public QObject
233-
{
234-
public:
235-
explicit DoubleShiftFilter(QAction *action, QObject *parent)
236-
: QObject(parent), m_action(action) {}
237-
238-
protected:
239-
bool eventFilter(QObject *, QEvent *event) override
240-
{
241-
const auto type = event->type();
242-
if (Q_LIKELY(type != QEvent::KeyRelease && type != QEvent::KeyPress))
243-
return false;
244-
auto *ke = static_cast<QKeyEvent *>(event);
245-
if (type == QEvent::KeyPress) {
246-
if (ke->key() != Qt::Key_Shift)
247-
m_lastShiftReleaseMs = 0;
248-
return false;
249-
}
250-
if (ke->key() != Qt::Key_Shift || ke->modifiers() != Qt::NoModifier || ke->isAutoRepeat())
251-
return false;
252-
const qint64 now = QDateTime::currentMSecsSinceEpoch();
253-
if (now - m_lastShiftReleaseMs < 400) {
254-
m_lastShiftReleaseMs = 0;
255-
if (m_action && m_action->isEnabled())
256-
m_action->trigger();
257-
} else {
258-
m_lastShiftReleaseMs = now;
259-
}
260-
return false;
261-
}
262-
263-
private:
264-
QAction *m_action;
265-
qint64 m_lastShiftReleaseMs = 0;
266-
};
267-
268232
} // namespace
269233

270234

@@ -451,7 +415,6 @@ MainWindow::MainWindow(NotepadNextApplication *app) :
451415
});
452416
dlg->show();
453417
});
454-
qApp->installEventFilter(new DoubleShiftFilter(m_actionQuickFileOpen, this));
455418
connect(this, &MainWindow::activeWorkspaceChanged, m_actionQuickFileOpen, [this]() {
456419
m_actionQuickFileOpen->setEnabled(!currentWorkspaceRoot().isEmpty());
457420
});

src/dialogs/QuickFileOpenDialog.cpp

Lines changed: 156 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,93 @@
88
#include <QStringView>
99
#include <QtConcurrent>
1010
#include <QApplication>
11+
#include <QPainter>
12+
#include <QStandardItemModel>
13+
#include <QStyledItemDelegate>
1114
#include <algorithm>
1215

16+
namespace {
17+
18+
class HighlightDelegate : public QStyledItemDelegate
19+
{
20+
public:
21+
using QStyledItemDelegate::QStyledItemDelegate;
22+
23+
void paint(QPainter *painter, const QStyleOptionViewItem &option,
24+
const QModelIndex &index) const override
25+
{
26+
QStyleOptionViewItem opt = option;
27+
initStyleOption(&opt, index);
28+
29+
painter->save();
30+
31+
if (opt.state & QStyle::State_Selected)
32+
painter->fillRect(opt.rect, opt.palette.highlight());
33+
else if (opt.state & QStyle::State_MouseOver)
34+
painter->fillRect(opt.rect, opt.palette.highlight().color().lighter(160));
35+
36+
const QString text = index.data(Qt::DisplayRole).toString();
37+
const auto positions = index.data(QuickFileOpenDialog::MatchPositionsRole).value<QVector<int>>();
38+
39+
const QRect textRect = opt.rect.adjusted(4, 0, -4, 0);
40+
const QFont font = opt.font;
41+
const QFontMetrics fm(font);
42+
43+
const QColor normalColor = (opt.state & QStyle::State_Selected)
44+
? opt.palette.highlightedText().color()
45+
: opt.palette.text().color();
46+
const QColor matchColor = QColor(79, 193, 255);
47+
48+
int posIdx = 0;
49+
const int posCount = positions.size();
50+
int x = textRect.left();
51+
const int y = textRect.top();
52+
const int h = textRect.height();
53+
int runStart = 0;
54+
const int textLen = text.length();
55+
56+
painter->setFont(font);
57+
painter->setClipRect(textRect);
58+
59+
while (runStart < textLen && x < textRect.right()) {
60+
bool isMatch = (posIdx < posCount && positions[posIdx] == runStart);
61+
62+
int runEnd = runStart + 1;
63+
if (isMatch) {
64+
++posIdx;
65+
while (runEnd < textLen && posIdx < posCount && positions[posIdx] == runEnd) {
66+
++posIdx;
67+
++runEnd;
68+
}
69+
} else {
70+
int nextMatch = (posIdx < posCount) ? positions[posIdx] : textLen;
71+
runEnd = nextMatch;
72+
}
73+
74+
const QStringView run = QStringView(text).mid(runStart, runEnd - runStart);
75+
const int runWidth = fm.horizontalAdvance(run.toString());
76+
77+
painter->setPen(isMatch ? matchColor : normalColor);
78+
painter->drawText(QRect(x, y, runWidth, h), Qt::AlignVCenter, run.toString());
79+
80+
x += runWidth;
81+
runStart = runEnd;
82+
}
83+
84+
painter->restore();
85+
}
86+
87+
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
88+
{
89+
QStyleOptionViewItem opt = option;
90+
initStyleOption(&opt, index);
91+
const QFontMetrics fm(opt.font);
92+
return QSize(opt.rect.width(), fm.height() + 4);
93+
}
94+
};
95+
96+
} // namespace
97+
1398
QuickFileOpenDialog::QuickFileOpenDialog(const QString &rootPath, QWidget *parent)
1499
: QDialog(parent, Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint)
15100
, m_rootPath(rootPath)
@@ -29,8 +114,9 @@ QuickFileOpenDialog::QuickFileOpenDialog(const QString &rootPath, QWidget *paren
29114
m_listView = new QListView(this);
30115
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
31116
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
32-
m_model = new QStringListModel(this);
117+
m_model = new QStandardItemModel(this);
33118
m_listView->setModel(m_model);
119+
m_listView->setItemDelegate(new HighlightDelegate(m_listView));
34120
layout->addWidget(m_listView);
35121

36122
m_lineEdit->installEventFilter(this);
@@ -107,13 +193,13 @@ bool QuickFileOpenDialog::eventFilter(QObject *obj, QEvent *event)
107193
case Qt::Key_Down: {
108194
int row = m_listView->currentIndex().row() + 1;
109195
if (row < m_model->rowCount())
110-
m_listView->setCurrentIndex(m_model->index(row));
196+
m_listView->setCurrentIndex(m_model->index(row, 0));
111197
return true;
112198
}
113199
case Qt::Key_Up: {
114200
int row = m_listView->currentIndex().row() - 1;
115201
if (row >= 0)
116-
m_listView->setCurrentIndex(m_model->index(row));
202+
m_listView->setCurrentIndex(m_model->index(row, 0));
117203
return true;
118204
}
119205
case Qt::Key_Return:
@@ -178,29 +264,42 @@ QStringList QuickFileOpenDialog::buildFileIndex(const QString &rootPath)
178264

179265
void QuickFileOpenDialog::applyFilter(const QString &pattern)
180266
{
267+
m_model->clear();
268+
181269
if (pattern.isEmpty()) {
182270
m_filteredFiles = m_allFiles.mid(0, 200);
271+
for (const QString &file : m_filteredFiles) {
272+
auto *item = new QStandardItem(file);
273+
item->setData(QVariant::fromValue(QVector<int>()), MatchPositionsRole);
274+
m_model->appendRow(item);
275+
}
183276
} else {
184-
struct Scored { int score; int idx; };
277+
struct Scored { int score; int idx; QVector<int> positions; };
185278
QVector<Scored> scored;
186279
scored.reserve(m_allFiles.size());
280+
QVector<int> positions;
187281
for (qsizetype i = 0, n = m_allFiles.size(); i < n; ++i) {
188-
int s = fuzzyScore(pattern, m_allFiles[i]);
282+
positions.clear();
283+
int s = fuzzyMatch(pattern, m_allFiles[i], positions);
189284
if (s > 0)
190-
scored.append({s, static_cast<int>(i)});
285+
scored.append({s, static_cast<int>(i), positions});
191286
}
192287
std::sort(scored.begin(), scored.end(), [](const Scored &a, const Scored &b) {
193288
return a.score > b.score;
194289
});
195-
m_filteredFiles.clear();
196290
const int limit = static_cast<int>(qMin(scored.size(), qsizetype(200)));
291+
m_filteredFiles.clear();
197292
m_filteredFiles.reserve(limit);
198-
for (int i = 0; i < limit; ++i)
199-
m_filteredFiles.append(m_allFiles[scored[i].idx]);
293+
for (int i = 0; i < limit; ++i) {
294+
const auto &entry = scored[i];
295+
m_filteredFiles.append(m_allFiles[entry.idx]);
296+
auto *item = new QStandardItem(m_allFiles[entry.idx]);
297+
item->setData(QVariant::fromValue(entry.positions), MatchPositionsRole);
298+
m_model->appendRow(item);
299+
}
200300
}
201-
m_model->setStringList(m_filteredFiles);
202301
if (!m_filteredFiles.isEmpty())
203-
m_listView->setCurrentIndex(m_model->index(0));
302+
m_listView->setCurrentIndex(m_model->index(0, 0));
204303
}
205304

206305
int QuickFileOpenDialog::fuzzyScore(const QString &pattern, const QString &candidate)
@@ -243,3 +342,49 @@ int QuickFileOpenDialog::fuzzyScore(const QString &pattern, const QString &candi
243342

244343
return score;
245344
}
345+
346+
int QuickFileOpenDialog::fuzzyMatch(const QString &pattern, const QString &candidate, QVector<int> &positions)
347+
{
348+
const int pLen = pattern.length();
349+
const int cLen = candidate.length();
350+
if (pLen == 0) return 1;
351+
if (pLen > cLen) return 0;
352+
353+
positions.resize(pLen);
354+
int score = 0;
355+
int pi = 0;
356+
int prevMatchPos = -2;
357+
358+
for (int ci = 0; ci < cLen && pi < pLen; ++ci) {
359+
const QChar pc = pattern[pi].toLower();
360+
const QChar cc = candidate[ci].toLower();
361+
if (pc == cc) {
362+
score += 1;
363+
if (ci == prevMatchPos + 1)
364+
score += 2;
365+
if (ci == 0 || candidate[ci - 1] == QLatin1Char('/') ||
366+
candidate[ci - 1] == QLatin1Char('\\') ||
367+
candidate[ci - 1] == QLatin1Char('.') ||
368+
candidate[ci - 1] == QLatin1Char('_') ||
369+
candidate[ci - 1] == QLatin1Char('-'))
370+
score += 3;
371+
if (candidate[ci].isUpper() && pattern[pi].isUpper())
372+
score += 2;
373+
positions[pi] = ci;
374+
prevMatchPos = ci;
375+
++pi;
376+
}
377+
}
378+
if (pi < pLen) {
379+
positions.clear();
380+
return 0;
381+
}
382+
383+
int filenameStart = candidate.lastIndexOf(QLatin1Char('/'));
384+
if (filenameStart < 0) filenameStart = 0; else ++filenameStart;
385+
const QStringView filename = QStringView(candidate).mid(filenameStart);
386+
if (filename.contains(pattern, Qt::CaseInsensitive))
387+
score += 10 + pLen;
388+
389+
return score;
390+
}

0 commit comments

Comments
 (0)