Skip to content

Commit 593068e

Browse files
feat(ai): pass chat history to prompt improver
- Force English output so improved prompts are always in a consistent language for downstream tooling. - Thread recent chat history into the improver request so the LLM has context when rewriting the draft; budget-capped at ~4000 chars walking backwards from the most recent message. - Replace SVG icon with a Unicode sparkle glyph to remove a palette-dependent repaint path on every theme change. - Make the improve button a cancel toggle while streaming; Esc also cancels, removing the need to disable the button. - Surface canImprove failure reason as a timed warning banner instead of silently doing nothing. - Rename the Preferences group box to "AI Provider" and add a note listing which features share the configuration, since more than commit messages now depend on it. - Improve canImprove error strings to be more actionable.
1 parent b49760b commit 593068e

8 files changed

Lines changed: 100 additions & 36 deletions

File tree

src/ai/PromptImprover.cpp

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ const QString kSystemTemplate = QStringLiteral(
2626
"- If the prompt starts with a slash command (e.g. /chore, /feat, /fix, "
2727
"/refactor), you MUST keep that command at the beginning. Never remove, "
2828
"rename, or reorder it.\n"
29-
"- Preserve the user's language (if they write in Vietnamese, respond in "
30-
"Vietnamese; English stays English).\n"
29+
"- Always output the improved prompt in English, regardless of the input "
30+
"language.\n"
3131
"- Do not add information the user did not mention. You may restructure, "
3232
"clarify ambiguity, add specificity where the intent is obvious, and "
3333
"improve phrasing.\n"
@@ -71,17 +71,18 @@ bool PromptImprover::canImprove(QString *whyNot) const
7171
if (m_state == State::Streaming)
7272
return fail(tr("Improvement already in progress"));
7373
if (m_settings->commitMessageProviderUrl().trimmed().isEmpty())
74-
return fail(tr("Configure the AI provider URL in Preferences → AI"));
74+
return fail(tr("AI provider not configured. Set the endpoint in Preferences → AI Provider."));
7575
if (m_settings->commitMessageModel().trimmed().isEmpty())
76-
return fail(tr("Configure the AI model in Preferences → AI"));
76+
return fail(tr("AI model not configured. Set the model in Preferences → AI Provider."));
7777
if (m_credStore && !m_credStore->isApiKeyAvailable())
78-
return fail(tr("Configure the AI API key in Preferences → AI"));
78+
return fail(tr("AI API key not configured. Save your key in Preferences → AI Provider."));
7979
return true;
8080
}
8181

8282
void PromptImprover::trigger(const QString &userDraft,
8383
const QString &workingDirectory,
84-
const QList<AcpProtocol::AcpCommandInfo> &commands)
84+
const QList<AcpProtocol::AcpCommandInfo> &commands,
85+
const QString &chatHistory)
8586
{
8687
QString why;
8788
if (!canImprove(&why)) {
@@ -104,12 +105,18 @@ void PromptImprover::trigger(const QString &userDraft,
104105

105106
m_responseBuffer.clear();
106107

108+
QString userMessage;
109+
if (!chatHistory.isEmpty()) {
110+
userMessage += QStringLiteral("<chat_history>\n%1\n</chat_history>\n\n").arg(chatHistory);
111+
}
112+
userMessage += QStringLiteral("<user_draft>\n%1\n</user_draft>").arg(userDraft);
113+
107114
ILlmHttpClient::Request req;
108115
req.url = QUrl(m_settings->commitMessageProviderUrl());
109116
req.model = m_settings->commitMessageModel();
110117
req.apiKey = apiKey;
111118
req.systemPrompt = systemPrompt;
112-
req.prompt = QStringLiteral("<user_draft>\n%1\n</user_draft>").arg(userDraft);
119+
req.prompt = userMessage;
113120
req.maxTokens = 4096;
114121
req.idleTimeoutSec = m_settings->commitMessageStreamIdleTimeoutSec();
115122

src/ai/PromptImprover.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ class PromptImprover : public QObject
3434

3535
void trigger(const QString &userDraft,
3636
const QString &workingDirectory,
37-
const QList<AcpProtocol::AcpCommandInfo> &commands);
37+
const QList<AcpProtocol::AcpCommandInfo> &commands,
38+
const QString &chatHistory = {});
3839

3940
void cancel();
4041

src/dialogs/PreferencesDialog.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,19 @@ PreferencesDialog::PreferencesDialog(ApplicationSettings *settings, QWidget *par
282282
offerRestart();
283283
});
284284

285-
// --- AI commit-message settings ------------------------------------------
285+
// --- AI provider settings --------------------------------------------------
286+
287+
// Add a note explaining which features use this configuration.
288+
{
289+
auto *noteLabel = new QLabel(
290+
tr("Used by: AI Commit Message, Prompt Improver. "
291+
"More features will use this provider in the future."),
292+
this);
293+
noteLabel->setWordWrap(true);
294+
noteLabel->setStyleSheet(QStringLiteral(
295+
"color: palette(placeholder-text); font-size: 11px; margin-bottom: 4px;"));
296+
ui->formLayoutAi->insertRow(0, noteLabel);
297+
}
286298

287299
ui->lineEditAiUrl->setText(settings->commitMessageProviderUrl());
288300
connect(ui->lineEditAiUrl, &QLineEdit::editingFinished, this, [=]() {

src/dialogs/PreferencesDialog.ui

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@
357357
<item>
358358
<widget class="QGroupBox" name="groupBoxAi">
359359
<property name="title">
360-
<string>AI Commit Message</string>
360+
<string>AI Provider</string>
361361
</property>
362362
<layout class="QFormLayout" name="formLayoutAi">
363363
<item row="0" column="0">

src/icons/sparkle.svg

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/resources.qrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
<file>icons/plus.svg</file>
4848
<file>icons/list_with_icons.svg</file>
4949
<file>icons/paperclip.svg</file>
50-
<file>icons/sparkle.svg</file>
5150
<file>icons/markdown-preview.svg</file>
5251
</qresource>
5352
</RCC>

src/widgets/AcpSessionView.cpp

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ AcpSessionView::AcpSessionView(AcpSessionModel *model,
205205

206206
AcpSessionView::~AcpSessionView() = default;
207207

208+
QSize AcpSessionView::sizeHint() const
209+
{
210+
return QSize(420, 300);
211+
}
212+
208213
QSize AcpSessionView::minimumSizeHint() const
209214
{
210215
return QSize(200, 100);
@@ -434,6 +439,13 @@ void AcpSessionView::buildUi()
434439
cb->onSubmit = [this]() { onSendClicked(); };
435440
cb->onRestart = [this]() { emit restartSessionRequested(); };
436441
cb->onKeyFilter = [this](QKeyEvent *ke) -> bool {
442+
// Esc cancels prompt improvement if streaming.
443+
if (ke->key() == Qt::Key_Escape
444+
&& m_promptImprover
445+
&& m_promptImprover->state() == ai::PromptImprover::State::Streaming) {
446+
onImproveClicked(); // toggles to cancel
447+
return true;
448+
}
437449
if (!m_commandPopup || !m_commandPopup->isVisible())
438450
return false;
439451
switch (ke->key()) {
@@ -496,16 +508,16 @@ void AcpSessionView::buildUi()
496508
// 6b. Floating improve-prompt button (child of m_input, bottom-right).
497509
m_improveBtn = new QToolButton(m_input->viewport());
498510
m_improveBtn->setAutoRaise(true);
499-
m_improveBtn->setToolButtonStyle(Qt::ToolButtonIconOnly);
511+
m_improveBtn->setToolButtonStyle(Qt::ToolButtonTextOnly);
512+
m_improveBtn->setText(QStringLiteral(""));
500513
m_improveBtn->setToolTip(tr("Improve prompt with AI (Ctrl+I)"));
501514
m_improveBtn->setAccessibleName(tr("Improve prompt with AI"));
502515
m_improveBtn->setAccessibleDescription(tr("Rewrite the current prompt to be clearer (Ctrl+I)"));
503516
m_improveBtn->setCursor(Qt::PointingHandCursor);
504517
m_improveBtn->setStyleSheet(QStringLiteral(
505-
"QToolButton { background: transparent; border: none; padding: 2px; border-radius: 3px; }"
518+
"QToolButton { background: transparent; border: none; padding: 2px; border-radius: 3px; font-size: 14px; }"
506519
"QToolButton:hover { background: palette(midlight); }"
507520
"QToolButton:disabled { opacity: 0.3; }"));
508-
rebuildImproveIcon();
509521
m_improveBtn->hide();
510522
connect(m_improveBtn, &QToolButton::clicked, this, &AcpSessionView::onImproveClicked);
511523

@@ -1751,7 +1763,6 @@ void AcpSessionView::changeEvent(QEvent *event)
17511763
case QEvent::StyleChange:
17521764
case QEvent::ApplicationPaletteChange:
17531765
rebuildAttachIcon();
1754-
rebuildImproveIcon();
17551766
break;
17561767
default:
17571768
break;
@@ -1765,13 +1776,6 @@ void AcpSessionView::rebuildAttachIcon()
17651776
palette().color(QPalette::WindowText)));
17661777
}
17671778

1768-
void AcpSessionView::rebuildImproveIcon()
1769-
{
1770-
if (!m_improveBtn) return;
1771-
m_improveBtn->setIcon(tintedIcon(QStringLiteral(":/icons/sparkle.svg"),
1772-
palette().color(QPalette::WindowText)));
1773-
}
1774-
17751779
void AcpSessionView::applyChatFont()
17761780
{
17771781
auto *settings = appSettings();
@@ -1914,16 +1918,29 @@ void AcpSessionView::updateImproveButtonState()
19141918

19151919
m_improveBtn->show();
19161920
positionImproveButton();
1917-
1918-
// Disable when streaming or LLM not configured.
1919-
const bool canFire = m_promptImprover && m_promptImprover->canImprove();
1920-
m_improveBtn->setEnabled(canFire);
1921+
m_improveBtn->setEnabled(true);
19211922
}
19221923

19231924
void AcpSessionView::onImproveClicked()
19241925
{
19251926
if (!m_promptImprover || !m_input) return;
1926-
if (!m_promptImprover->canImprove()) return;
1927+
1928+
// If already streaming, cancel.
1929+
if (m_promptImprover->state() == ai::PromptImprover::State::Streaming) {
1930+
m_promptImprover->cancel();
1931+
m_input->setReadOnly(false);
1932+
m_improveBtn->setText(QStringLiteral(""));
1933+
m_improveBtn->setToolTip(tr("Improve prompt with AI (Ctrl+I)"));
1934+
updateImproveButtonState();
1935+
return;
1936+
}
1937+
1938+
QString whyNot;
1939+
if (!m_promptImprover->canImprove(&whyNot)) {
1940+
setBanner(whyNot, BannerKind::Warning);
1941+
QTimer::singleShot(5000, this, &AcpSessionView::clearBanner);
1942+
return;
1943+
}
19271944

19281945
const QString draft = m_input->toPlainText().trimmed();
19291946
if (draft.isEmpty()) return;
@@ -1940,12 +1957,43 @@ void AcpSessionView::onImproveClicked()
19401957
const QList<AcpProtocol::AcpCommandInfo> commands =
19411958
m_model ? m_model->availableCommands() : QList<AcpProtocol::AcpCommandInfo>{};
19421959

1960+
// Build a condensed chat history from recent messages (budget: ~4000 chars).
1961+
QString chatHistory;
1962+
if (m_model) {
1963+
constexpr int kHistoryBudget = 4000;
1964+
const auto &msgs = m_model->messages();
1965+
int totalChars = 0;
1966+
// Walk backwards to get the most recent messages first.
1967+
for (int i = msgs.size() - 1; i >= 0; --i) {
1968+
const auto &msg = msgs[i];
1969+
if (msg.role != QLatin1String("user") && msg.role != QLatin1String("assistant"))
1970+
continue;
1971+
QString text;
1972+
for (const auto &block : msg.content) {
1973+
if (block.kind == AcpProtocol::AcpContentBlock::Kind::Text)
1974+
text += block.text;
1975+
}
1976+
text = text.trimmed();
1977+
if (text.isEmpty()) continue;
1978+
// Truncate individual messages that are too long.
1979+
if (text.size() > 800)
1980+
text = text.left(800) + QStringLiteral("...");
1981+
const QString entry = QStringLiteral("[%1]: %2\n").arg(msg.role, text);
1982+
if (totalChars + entry.size() > kHistoryBudget)
1983+
break;
1984+
chatHistory.prepend(entry);
1985+
totalChars += entry.size();
1986+
}
1987+
chatHistory = chatHistory.trimmed();
1988+
}
1989+
19431990
m_originalDraftBeforeImprove = m_input->toPlainText();
19441991
m_input->setReadOnly(true);
1945-
m_improveBtn->setEnabled(false);
1946-
m_improveBtn->setToolTip(tr("Improving prompt..."));
1992+
m_improveBtn->setText(QStringLiteral(""));
1993+
m_improveBtn->setToolTip(tr("Stop improving (Esc)"));
1994+
m_improveBtn->setEnabled(true);
19471995

1948-
m_promptImprover->trigger(draft, workingDir, commands);
1996+
m_promptImprover->trigger(draft, workingDir, commands, chatHistory);
19491997
}
19501998

19511999
void AcpSessionView::onImproveFinished(const QString &improvedText)
@@ -1961,6 +2009,7 @@ void AcpSessionView::onImproveFinished(const QString &improvedText)
19612009
cursor.endEditBlock();
19622010

19632011
m_input->setReadOnly(false);
2012+
m_improveBtn->setText(QStringLiteral(""));
19642013
m_improveBtn->setToolTip(tr("Improve prompt with AI (Ctrl+I)"));
19652014
m_input->setFocus();
19662015
updateImproveButtonState();
@@ -1971,6 +2020,7 @@ void AcpSessionView::onImproveError(const QString &message)
19712020
if (!m_input) return;
19722021

19732022
m_input->setReadOnly(false);
2023+
m_improveBtn->setText(QStringLiteral(""));
19742024
m_improveBtn->setToolTip(tr("Improve prompt with AI (Ctrl+I)"));
19752025
updateImproveButtonState();
19762026

src/widgets/AcpSessionView.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class AcpSessionView : public QWidget
103103
private slots:
104104
void onShowDebugLogClicked();
105105

106+
QSize sizeHint() const override;
106107
QSize minimumSizeHint() const override;
107108

108109
protected:
@@ -144,7 +145,6 @@ private slots:
144145
void buildUi();
145146
void wireSignals();
146147
void rebuildAttachIcon();
147-
void rebuildImproveIcon();
148148
void hydrateFromModel();
149149
void appendMessageWidget(int idx);
150150
// Insert a widget into the transcript timeline at the tail, just above

0 commit comments

Comments
 (0)