Skip to content

Commit 4a45e2b

Browse files
fix(ai): wire images into prompt improver and fix diff palette
- PromptImprover::trigger now accepts images and enforces a 10 MB budget, emitting imagesBudgeted when images are dropped; callers use peekAll() so attachments survive the improve round-trip - LlmHttpClient::buildPayload emits multipart content array when images are present, embedding base64 data-URIs with detail=low - Diff palette dark canvasBg/Fg now matches the editor's own defaultBack/Fore so the diff canvas blends seamlessly; bgGutter added to both palettes to fix inverted gutter colors after theme transitions - applyDiffStyles extracted so EditorManager's lexerChanged handler can skip diff views rather than overwriting the diff palette with the chrome theme's defaultBack - openFolderAsWorkspacePath resolves relative paths to absolute before storing in the recent list or passing to QFileSystemModel, preventing "." from leaking into persisted state - AiAgentDock minimum width bumped to 600 to accommodate image thumbnails without clipping
1 parent 51cbc8e commit 4a45e2b

15 files changed

Lines changed: 226 additions & 84 deletions

src/EditorManager.cpp

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ void EditorManager::setupEditor(ScintillaNext *editor)
296296
// applied (lexerChanged is emitted *after* SetLanguage from
297297
// NotepadNextApplication::setEditorLanguage) so dark mode survives.
298298
connect(editor, &ScintillaNext::lexerChanged, this, [this, editor]() {
299+
// Diff views own their palette (GitDiffPainter); the chrome theme's
300+
// defaultBack does not match canvasBg, so applying it would corrupt
301+
// the diff colors until the next configureEditor call.
302+
if (isDiffView(editor)) return;
299303
applyThemeToEditor(editor, darkTheme, /*initialSetup=*/false);
300304
});
301305

@@ -372,17 +376,23 @@ void EditorManager::setupEditor(ScintillaNext *editor)
372376
JustfileRecipeHighlighter *jrh = new JustfileRecipeHighlighter(editor);
373377
jrh->setEnabled(true);
374378

375-
GitGutterDecorator *gg = new GitGutterDecorator(editor);
376-
gg->setEnabled(gitGutterEnabled);
379+
const bool isDiff = isDiffView(editor);
377380

378-
InlineBlameDecorator *blame = new InlineBlameDecorator(editor);
379-
blame->setEnabled(inlineBlameEnabled);
380-
connect(blame, &InlineBlameDecorator::commitClicked,
381-
this, &EditorManager::blameCommitClicked);
381+
GitGutterDecorator *gg = nullptr;
382+
InlineBlameDecorator *blame = nullptr;
383+
if (!isDiff) {
384+
gg = new GitGutterDecorator(editor);
385+
gg->setEnabled(gitGutterEnabled);
386+
387+
blame = new InlineBlameDecorator(editor);
388+
blame->setEnabled(inlineBlameEnabled);
389+
connect(blame, &InlineBlameDecorator::commitClicked,
390+
this, &EditorManager::blameCommitClicked);
391+
}
382392

383393
// Defer git decorator refreshes further so the deferred batch
384394
// itself doesn't block on QProcess spawns.
385-
if (isFile) {
395+
if (isFile && !isDiff) {
386396
QTimer::singleShot(0, editor, [gg, blame, gitGutterEnabled, inlineBlameEnabled]() {
387397
if (gitGutterEnabled) gg->refresh();
388398
if (inlineBlameEnabled) blame->refresh();
@@ -405,6 +415,14 @@ void EditorManager::applyTheme(bool dark)
405415
for (auto &editor : getEditors()) {
406416
if (!editor) continue;
407417

418+
// Diff views own their palette via GitDiffPainter::configureEditor and
419+
// are re-skinned by GitDiffViewController::setDarkPalette on the
420+
// effectiveThemeChanged signal. Running applyThemeToEditor on them
421+
// would overwrite canvasBg/canvasFg with the chrome's defaultBack
422+
// (visible as gray banding under the diff content) until the diff
423+
// controller's slot fires next tick.
424+
if (isDiffView(editor)) continue;
425+
408426
if (transitionToLight) {
409427
// Coming from dark, where applyThemeToEditor lightened the lexer
410428
// foregrounds in-place. The original lexer-written values were

src/NotepadNextApplication.cpp

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747

4848
#include <QCommandLineParser>
4949

50+
#include <QDir>
5051
#include <QDirIterator>
52+
#include <QFileInfo>
5153
#include <QGuiApplication>
5254
#include <QPalette>
5355
#include <QStyle>
@@ -87,6 +89,27 @@ static QString toLocalFileName(const QString file)
8789
return fileUrl.isValid() && fileUrl.isLocalFile() ? fileUrl.toLocalFile() : file;
8890
}
8991

92+
// Drop entries that can never be valid workspaces (empty, relative like ".",
93+
// or paths whose target directory no longer exists) and resolve any survivor
94+
// to its absolute form. Stale relative paths can land in App/RecentWorkspacesList
95+
// or FolderAsWorkspace/Workspaces from older builds that didn't normalize on
96+
// write — without this filter they'd keep showing up in the recent menu and
97+
// would re-spawn empty docks on session restore.
98+
static QStringList sanitizeWorkspacePaths(const QStringList &raw)
99+
{
100+
QStringList out;
101+
out.reserve(raw.size());
102+
for (const QString &p : raw) {
103+
if (p.isEmpty()) continue;
104+
const QString abs = QDir(p).absolutePath();
105+
if (abs.isEmpty()) continue;
106+
if (!QFileInfo(abs).isDir()) continue;
107+
if (out.contains(abs)) continue;
108+
out.append(abs);
109+
}
110+
return out;
111+
}
112+
90113
// Detect --new-window from raw argv before SingleApplication runs its primary-instance
91114
// check. When set we hand the base ctor a unique userData so this process gets its own
92115
// block-server identity and isn't intercepted as a secondary of the existing instance.
@@ -304,8 +327,9 @@ bool NotepadNextApplication::init()
304327
// key is no longer read or written.
305328
if (settings->value("FolderAsWorkspace/Workspaces").toStringList().isEmpty()) {
306329
const QString legacyRoot = settings->value("FolderAsWorkspace/RootPath").toString();
307-
if (!legacyRoot.isEmpty()) {
308-
settings->setValue("FolderAsWorkspace/Workspaces", QStringList{legacyRoot});
330+
const QStringList sanitized = sanitizeWorkspacePaths(QStringList{legacyRoot});
331+
if (!sanitized.isEmpty()) {
332+
settings->setValue("FolderAsWorkspace/Workspaces", sanitized);
309333
}
310334
}
311335

@@ -670,7 +694,8 @@ void NotepadNextApplication::openFiles(const QStringList &files)
670694
void NotepadNextApplication::loadSettings()
671695
{
672696
recentFilesListManager->setFileList(getSettings()->value("App/RecentFilesList").toStringList());
673-
recentWorkspacesListManager->setFileList(getSettings()->value("App/RecentWorkspacesList").toStringList());
697+
recentWorkspacesListManager->setFileList(
698+
sanitizeWorkspacePaths(getSettings()->value("App/RecentWorkspacesList").toStringList()));
674699
}
675700

676701
void NotepadNextApplication::saveSettings()

src/ai/ILlmHttpClient.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
#define AI_I_LLM_HTTP_CLIENT_H
2121

2222
#include <QObject>
23+
#include <QPair>
2324
#include <QString>
2425
#include <QUrl>
26+
#include <QVector>
2527

2628
namespace ai {
2729

@@ -40,6 +42,7 @@ class ILlmHttpClient : public QObject
4042
QString prompt; // already-assembled full prompt (user role)
4143
int maxTokens = 0; // 0 = omit from payload (provider default)
4244
int idleTimeoutSec = 60;
45+
QVector<QPair<QByteArray, QString>> images;
4346
};
4447

4548
explicit ILlmHttpClient(QObject *parent = nullptr) : QObject(parent) {}

src/ai/LlmHttpClient.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,28 @@ QByteArray LlmHttpClient::buildPayload(const Request &req)
5959

6060
QJsonObject userMsg;
6161
userMsg.insert(QLatin1String("role"), QLatin1String("user"));
62-
userMsg.insert(QLatin1String("content"), req.prompt);
62+
if (req.images.isEmpty()) {
63+
userMsg.insert(QLatin1String("content"), req.prompt);
64+
} else {
65+
QJsonArray contentArr;
66+
QJsonObject textPart;
67+
textPart.insert(QLatin1String("type"), QLatin1String("text"));
68+
textPart.insert(QLatin1String("text"), req.prompt);
69+
contentArr.append(textPart);
70+
for (const auto &img : req.images) {
71+
QJsonObject urlObj;
72+
urlObj.insert(QLatin1String("url"),
73+
QStringLiteral("data:%1;base64,%2")
74+
.arg(img.second,
75+
QString::fromLatin1(img.first.toBase64())));
76+
urlObj.insert(QLatin1String("detail"), QLatin1String("low"));
77+
QJsonObject imgPart;
78+
imgPart.insert(QLatin1String("type"), QLatin1String("image_url"));
79+
imgPart.insert(QLatin1String("image_url"), urlObj);
80+
contentArr.append(imgPart);
81+
}
82+
userMsg.insert(QLatin1String("content"), contentArr);
83+
}
6384
messages.append(userMsg);
6485

6586
QJsonObject body;

src/ai/PromptImprover.cpp

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ bool PromptImprover::canImprove(QString *whyNot) const
8282
void PromptImprover::trigger(const QString &userDraft,
8383
const QString &workingDirectory,
8484
const QList<AcpProtocol::AcpCommandInfo> &commands,
85-
const QString &chatHistory)
85+
const QString &chatHistory,
86+
const QVector<QPair<QByteArray, QString>> &images)
8687
{
8788
QString why;
8889
if (!canImprove(&why)) {
@@ -101,7 +102,20 @@ void PromptImprover::trigger(const QString &userDraft,
101102
}
102103

103104
const QString rules = loadRules(workingDirectory);
104-
const QString systemPrompt = buildSystemPrompt(rules, commands);
105+
106+
constexpr qint64 kImageBudget = 10LL * 1024 * 1024;
107+
QVector<QPair<QByteArray, QString>> budgetedImages;
108+
qint64 totalBytes = 0;
109+
for (const auto &img : images) {
110+
if (totalBytes + img.first.size() > kImageBudget) break;
111+
budgetedImages.append(img);
112+
totalBytes += img.first.size();
113+
}
114+
if (budgetedImages.size() < images.size()) {
115+
emit imagesBudgeted(budgetedImages.size(), images.size());
116+
}
117+
118+
const QString systemPrompt = buildSystemPrompt(rules, commands, budgetedImages.size());
105119

106120
m_responseBuffer.clear();
107121

@@ -119,6 +133,7 @@ void PromptImprover::trigger(const QString &userDraft,
119133
req.prompt = userMessage;
120134
req.maxTokens = 4096;
121135
req.idleTimeoutSec = m_settings->commitMessageStreamIdleTimeoutSec();
136+
req.images = budgetedImages;
122137

123138
setState(State::Streaming);
124139
m_http->openStream(req);
@@ -172,10 +187,21 @@ void PromptImprover::onStreamError(int httpStatus, const QString &message)
172187

173188
QString PromptImprover::buildSystemPrompt(
174189
const QString &rules,
175-
const QList<AcpProtocol::AcpCommandInfo> &commands) const
190+
const QList<AcpProtocol::AcpCommandInfo> &commands,
191+
int imageCount) const
176192
{
177193
QString prompt = kSystemTemplate;
178194

195+
if (imageCount > 0) {
196+
prompt += QStringLiteral(
197+
"- The user has attached %1 image(s) alongside their draft. Examine the "
198+
"visual content and incorporate what you see into the improved prompt — "
199+
"reference specific UI elements, layouts, errors, or details visible in "
200+
"the images to make the prompt more concrete. Do not narrate what you see "
201+
"in the images; weave their context into the rewritten prompt naturally.\n")
202+
.arg(imageCount);
203+
}
204+
179205
if (!rules.isEmpty()) {
180206
prompt += QStringLiteral("\n<project_rules>\n%1\n</project_rules>\n").arg(rules);
181207
}

src/ai/PromptImprover.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
#define AI_PROMPT_IMPROVER_H
33

44
#include <QObject>
5+
#include <QPair>
56
#include <QString>
67
#include <QList>
8+
#include <QVector>
79

810
#include <cstdint>
911

@@ -35,14 +37,16 @@ class PromptImprover : public QObject
3537
void trigger(const QString &userDraft,
3638
const QString &workingDirectory,
3739
const QList<AcpProtocol::AcpCommandInfo> &commands,
38-
const QString &chatHistory = {});
40+
const QString &chatHistory = {},
41+
const QVector<QPair<QByteArray, QString>> &images = {});
3942

4043
void cancel();
4144

4245
signals:
4346
void stateChanged(ai::PromptImprover::State state);
4447
void finished(const QString &improvedText);
4548
void errorOccurred(const QString &message);
49+
void imagesBudgeted(int sent, int total);
4650

4751
private slots:
4852
void onFirstByte();
@@ -53,7 +57,8 @@ private slots:
5357
private:
5458
void setState(State s);
5559
QString buildSystemPrompt(const QString &rules,
56-
const QList<AcpProtocol::AcpCommandInfo> &commands) const;
60+
const QList<AcpProtocol::AcpCommandInfo> &commands,
61+
int imageCount) const;
5762
QString loadRules(const QString &workingDirectory) const;
5863
QString serializeCommands(const QList<AcpProtocol::AcpCommandInfo> &commands) const;
5964
QString parseImprovedPrompt(const QString &response) const;

0 commit comments

Comments
 (0)