Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/conf/Setting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ void Setting::initialize(QMap<Id, QString> &keys) {
keys[Id::ShowChangedFilesInSingleView] = "doubletreeview/single";
keys[Id::ShowChangedFilesMultiColumn] = "doubletreeview/listviewmulticolumn";
keys[Id::HideUntracked] = "untracked.hide";
keys[Id::AiServiceUrl] = "ai/service/url";
keys[Id::EnableAiCommitMessages] = "ai/commit/enable";
keys[Id::AiCommitModel] = "ai/commit/model";
keys[Id::AiCommitTemperature] = "ai/commit/temperature";
keys[Id::AiCommitSystemMessage] = "ai/commit/system_message";
keys[Id::AiCommitPromptMessage] = "ai/commit/prompt_message";
}

void Prompt::initialize(QMap<Kind, QString> &keys) {
Expand Down
48 changes: 48 additions & 0 deletions src/conf/Setting.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,48 @@
#include <QObject>
#include <QMap>

// AI Commit Message Generation Constants
namespace AiCommitConstants {

inline constexpr bool enabled = true;
inline constexpr const char *DefaultServiceUrl = "https://api.mistral.ai/v1";
inline constexpr const char *DefaultModel = "mistral-tiny";
inline constexpr double DefaultTemperature = 0.2;

inline constexpr const char *DefaultSystemMessage =
"You are a helpful assistant that generates concise git commit messages "
"following conventional commits format. "
"Start directly with the commit message without any prefixes like 'Commit "
"Message:', 'Git', or markdown code blocks. "
"Follow the format: type(scope): subject\n\nbody";

inline constexpr const char *DefaultPromptMessage =
"Generate a concise, professional git commit message based on the "
"following changes:\n\n"
"Changed files:\n"
"{{CHANGED_FILES}}\n"
"Additions:\n"
"{{ADDITIONS}}\n"
"Deletions:\n"
"{{DELETIONS}}\n"
"File changes:\n"
"{{FILE_CHANGES}}\n"
"Total changes: +{{TOTAL_ADDITIONS}} lines, -{{TOTAL_DELETIONS}} "
"lines\n\n"
"Follow git commit message conventions:\n"
"- First line: short summary (50-72 chars max)\n"
"- Body: detailed explanation (72 chars per line max)\n"
"- Use imperative mood (e.g., 'Fix bug' not 'Fixed bug')\n"
"- Be concise but descriptive\n"
"- Start directly with the commit message (no prefixes like 'Commit "
"Message:' nor markdown in the title)\n"
"- Follow format: type(scope): subject\\n\\nbody\n"
"- Consider the commit message idea if available\n"
"\n"
"Generate the commit message:";

} // namespace AiCommitConstants

template <class T> class SettingsTempl {
public:
template <typename TId> static QString key(const TId id) {
Expand Down Expand Up @@ -68,6 +110,12 @@ class Setting : public SettingsTempl<Setting> {
ShowChangedFilesInSingleView,
HideUntracked,
Language,
AiServiceUrl,
EnableAiCommitMessages,
AiCommitModel,
AiCommitTemperature,
AiCommitSystemMessage,
AiCommitPromptMessage,
};
Q_ENUM(Id)

Expand Down
128 changes: 128 additions & 0 deletions src/dialogs/SettingsDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
#include <QSpinBox>
#include <QStackedWidget>
#include <QStandardItemModel>
#include <QTextEdit>
#include <QToolBar>
#include <QMessageBox>

Expand Down Expand Up @@ -792,10 +793,137 @@ class MiscPanel : public QWidget {
Settings::instance()->setValue(Setting::Id::SshKeyFilePath, text);
});

// AI Commit Message settings
QCheckBox *enableAiCommitMessages =
new QCheckBox(tr("Enable AI commit messages"), this);
enableAiCommitMessages->setChecked(
settings
->value(Setting::Id::EnableAiCommitMessages,
AiCommitConstants::enabled)
.toBool());
connect(enableAiCommitMessages, &QCheckBox::toggled, [](bool checked) {
Settings::instance()->setValue(Setting::Id::EnableAiCommitMessages,
checked);
});

// Initialize default AI service URL if not set
QString aiServiceUrl =
settings->value(Setting::Id::AiServiceUrl).toString();
if (aiServiceUrl.isEmpty()) {
aiServiceUrl = AiCommitConstants::DefaultServiceUrl;
settings->setValue(Setting::Id::AiServiceUrl, aiServiceUrl);
}

QLineEdit *aiServiceUrlBox = new QLineEdit(aiServiceUrl, this);
connect(aiServiceUrlBox, &QLineEdit::textChanged, [](const QString &text) {
Settings::instance()->setValue(Setting::Id::AiServiceUrl, text);
});

// AI Service API Key - stored securely using credential helper
QLineEdit *aiApiKeyBox = new QLineEdit(this);

// Load API key from credential helper using the API URL as key
CredentialHelper *credHelper = CredentialHelper::instance();
if (credHelper && !aiServiceUrl.isEmpty()) {
QString aiApiKey;
credHelper->get(aiServiceUrl, aiApiKey, aiApiKey);
if (!aiApiKey.isEmpty()) {
aiApiKeyBox->setText(tr("Api key found for specified api url"));
}
}

connect(
aiApiKeyBox, &QLineEdit::textChanged,
[aiServiceUrlBox, aiApiKeyBox](const QString &apiKey) {
QString aiServiceUrl = aiServiceUrlBox->text();
CredentialHelper *credHelper = CredentialHelper::instance();
if (credHelper && !apiKey.isEmpty() && !aiServiceUrl.isEmpty()) {
bool success =
credHelper->store(aiServiceUrl, "ai-commit-key", apiKey);
if (!success) {
QMessageBox::warning(
aiApiKeyBox, QObject::tr("Credential Storage Failed"),
QObject::tr("Failed to store API key in secure storage. The "
"key will not be saved."));
}
} else if (credHelper && apiKey.isEmpty() &&
!aiServiceUrl.isEmpty()) {
// If key is empty, remove it from storage
bool success = credHelper->store(aiServiceUrl, "ai-commit-key", "");
if (!success) {
QMessageBox::warning(
aiApiKeyBox, QObject::tr("Credential Removal Failed"),
QObject::tr("Failed to remove API key from secure storage."));
}
}
});

// Configurable AI settings
QLineEdit *aiCommitModelBox = new QLineEdit(
settings
->value(Setting::Id::AiCommitModel, AiCommitConstants::DefaultModel)
.toString(),
this);
connect(aiCommitModelBox, &QLineEdit::textChanged, [](const QString &text) {
Settings::instance()->setValue(Setting::Id::AiCommitModel, text);
});

// Hide temperature and system message for now
QDoubleSpinBox *aiCommitTemperatureBox = new QDoubleSpinBox(this);
aiCommitTemperatureBox->setRange(0.0, 2.0);
aiCommitTemperatureBox->setSingleStep(0.1);
aiCommitTemperatureBox->setValue(
settings
->value(Setting::Id::AiCommitTemperature,
AiCommitConstants::DefaultTemperature)
.toDouble());
aiCommitTemperatureBox->setVisible(false);
connect(aiCommitTemperatureBox,
QOverload<double>::of(&QDoubleSpinBox::valueChanged),
[](double value) {
Settings::instance()->setValue(Setting::Id::AiCommitTemperature,
value);
});

QTextEdit *aiCommitSystemMessageBox = new QTextEdit(this);
aiCommitSystemMessageBox->setPlainText(
settings
->value(Setting::Id::AiCommitSystemMessage,
QObject::tr(AiCommitConstants::DefaultSystemMessage))
.toString());
aiCommitSystemMessageBox->setVisible(false);
connect(aiCommitSystemMessageBox, &QTextEdit::textChanged,
[aiCommitSystemMessageBox]() {
Settings::instance()->setValue(
Setting::Id::AiCommitSystemMessage,
aiCommitSystemMessageBox->toPlainText());
});

// Add prompt message configuration
QTextEdit *aiCommitPromptMessageBox = new QTextEdit(this);
aiCommitPromptMessageBox->setPlainText(
settings
->value(Setting::Id::AiCommitPromptMessage,
AiCommitConstants::DefaultPromptMessage)
.toString());
connect(aiCommitPromptMessageBox, &QTextEdit::textChanged,
[aiCommitPromptMessageBox]() {
Settings::instance()->setValue(
Setting::Id::AiCommitPromptMessage,
aiCommitPromptMessageBox->toPlainText());
});

QFormLayout *layout = new QFormLayout(this);
layout->addRow(tr("Path to SSH config file:"), sshConfigPathBox);
layout->addRow(tr("Path to default / fallback SSH key file:"),
sshKeyPathBox);

layout->addRow(new QLabel(tr("<b>AI Commit Message Generation</b>")));
layout->addRow(tr("Enable AI commit messages:"), enableAiCommitMessages);
layout->addRow(tr("AI Service URL:"), aiServiceUrlBox);
layout->addRow(tr("AI Service API Key:"), aiApiKeyBox);
layout->addRow(tr("AI Commit Model:"), aiCommitModelBox);
layout->addRow(tr("AI Prompt Message:"), aiCommitPromptMessageBox);
}
};

Expand Down
Loading
Loading