Compare commits

...

6 commits

Author SHA1 Message Date
GadflyFang
c08c307c7a
enhance: add GitLab Issue/MR sample rules (#598)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
* enhance: add GitLab Issue/MR sample rules
* fix: remove blank in zh_CN

---------

Signed-off-by: Gadfly <gadfly@gadfly.vip>
2024-10-24 18:07:04 +08:00
leo
d3d71af0e8
enhance: only alloc temp buffer once time (#597)
Signed-off-by: leo <longshuang@msn.cn>
2024-10-24 17:07:30 +08:00
leo
339bceef3d
fix: files should be displayed after folders (#597)
Signed-off-by: leo <longshuang@msn.cn>
2024-10-24 17:00:03 +08:00
leo
76a7a2228f
feature: use numeric sorting for all trees (#597)
Signed-off-by: leo <longshuang@msn.cn>
2024-10-24 15:11:10 +08:00
leo
12bb915bd8
code_review: PR #596
- Add `ViewModels.Preference.PrepareOpenAIPrompt()` method to generate default prompt instead of a const fallback value. Therefore, it is more convenient for us to modify the default value in the Preference dialog.
- Modify the default prompts. Rename `SubjectPrompt` to `GenerateSubjectPrompt`. Rename `SummaryPrompt` to `AnalyzeDiffPrompt`.
- Rewrite the way to build OpenAI user content for subject generation

Signed-off-by: leo <longshuang@msn.cn>
2024-10-24 10:22:06 +08:00
Douglas Cunha
2f68aed817
feat: improve commit message generation with AI prompts (#596)
- Refactor the commit message generation process to utilize default prompts and enhance clarity while eliminating redundancy.
- Added new properties for subject and summary prompts, while improving cancellation support in async task handling.
- feat: add AI prompts for commit message generation.
- Updated the formatting of the package reference for consistency in the project file.
- Add properties for managing OpenAI subject and summary prompts in the Preference view model.
- Refactor layout and add new input fields for AI subject and summary prompts in the preferences view.
2024-10-24 09:31:05 +08:00
15 changed files with 274 additions and 58 deletions

View file

@ -32,31 +32,36 @@ namespace SourceGit.Commands
{
try
{
var summaries = new List<string>();
var summarybuilder = new StringBuilder();
var bodyBuilder = new StringBuilder();
foreach (var change in _changes)
{
if (_cancelToken.IsCancellationRequested)
return "";
_onProgress?.Invoke($"Analyzing {change.Path}...");
var summary = GenerateChangeSummary(change);
summaries.Add(summary);
summarybuilder.Append("- ");
summarybuilder.Append(summary);
summarybuilder.Append("(file: ");
summarybuilder.Append(change.Path);
summarybuilder.Append(")");
summarybuilder.AppendLine();
bodyBuilder.Append("- ");
bodyBuilder.Append(summary);
bodyBuilder.AppendLine();
}
if (_cancelToken.IsCancellationRequested)
return "";
_onProgress?.Invoke($"Generating commit message...");
var builder = new StringBuilder();
builder.Append(GenerateSubject(string.Join("", summaries)));
builder.Append("\n");
foreach (var summary in summaries)
{
builder.Append("\n- ");
builder.Append(summary.Trim());
}
return builder.ToString();
var body = bodyBuilder.ToString();
var subject = GenerateSubject(summarybuilder.ToString());
return string.Format("{0}\n\n{1}", subject, body);
}
catch (Exception e)
{
@ -70,15 +75,7 @@ namespace SourceGit.Commands
var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd();
var diff = rs.IsSuccess ? rs.StdOut : "unknown change";
var prompt = new StringBuilder();
prompt.AppendLine("You are an expert developer specialist in creating commits.");
prompt.AppendLine("Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:");
prompt.AppendLine("- Do not use any code snippets, imports, file routes or bullets points.");
prompt.AppendLine("- Do not mention the route of file that has been change.");
prompt.AppendLine("- Simply describe the MAIN GOAL of the changes.");
prompt.AppendLine("- Output directly the summary in plain text.`");
var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here is the `git diff` output: {diff}", _cancelToken);
var rsp = Models.OpenAI.Chat(Models.OpenAI.AnalyzeDiffPrompt, $"Here is the `git diff` output: {diff}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content;
@ -87,24 +84,7 @@ namespace SourceGit.Commands
private string GenerateSubject(string summary)
{
var prompt = new StringBuilder();
prompt.AppendLine("You are an expert developer specialist in creating commits messages.");
prompt.AppendLine("Your only goal is to retrieve a single commit message.");
prompt.AppendLine("Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:");
prompt.AppendLine("- Assign the commit {type} according to the next conditions:");
prompt.AppendLine(" feat: Only when adding a new feature.");
prompt.AppendLine(" fix: When fixing a bug.");
prompt.AppendLine(" docs: When updating documentation.");
prompt.AppendLine(" style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic.");
prompt.AppendLine(" test: When adding or updating tests. ");
prompt.AppendLine(" chore: When making changes to the build process or auxiliary tools and libraries. ");
prompt.AppendLine(" revert: When undoing a previous commit.");
prompt.AppendLine(" refactor: When restructuring code without changing its external behavior, or is any of the other refactor types.");
prompt.AppendLine("- Do not add any issues numeration, explain your output nor introduce your answer.");
prompt.AppendLine("- Output directly only one commit message in plain text with the next format: {type}: {commit_message}.");
prompt.AppendLine("- Be as concise as possible, keep the message under 50 characters.");
var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here are the summaries changes: {summary}", _cancelToken);
var rsp = Models.OpenAI.Chat(Models.OpenAI.GenerateSubjectPrompt, $"Here are the summaries changes:\n{summary}", _cancelToken);
if (rsp != null && rsp.Choices.Count > 0)
return rsp.Choices[0].Message.Content;

70
src/Models/NumericSort.cs Normal file
View file

@ -0,0 +1,70 @@
namespace SourceGit.Models
{
public static class NumericSort
{
public static int Compare(string s1, string s2)
{
int len1 = s1.Length;
int len2 = s2.Length;
int marker1 = 0;
int marker2 = 0;
char[] tmp1 = new char[len1];
char[] tmp2 = new char[len2];
while (marker1 < len1 && marker2 < len2)
{
char c1 = s1[marker1];
char c2 = s2[marker2];
int loc1 = 0;
int loc2 = 0;
bool isDigit1 = char.IsDigit(c1);
do
{
tmp1[loc1] = c1;
loc1++;
marker1++;
if (marker1 < len1)
c1 = s1[marker1];
else
break;
} while (char.IsDigit(c1) == isDigit1);
bool isDigit2 = char.IsDigit(c2);
do
{
tmp2[loc2] = c2;
loc2++;
marker2++;
if (marker2 < len2)
c2 = s2[marker2];
else
break;
} while (char.IsDigit(c2) == isDigit2);
string sub1 = new string(tmp1, 0, loc1);
string sub2 = new string(tmp2, 0, loc2);
int result;
if (isDigit1 && isDigit2)
{
int num1 = int.Parse(sub1);
int num2 = int.Parse(sub2);
result = num1 - num2;
}
else
{
result = string.Compare(sub1, sub2, System.StringComparison.Ordinal);
}
if (result != 0)
return result;
}
return len1 - len2;
}
}
}

View file

@ -94,6 +94,18 @@ namespace SourceGit.Models
set;
}
public static string AnalyzeDiffPrompt
{
get;
set;
}
public static string GenerateSubjectPrompt
{
get;
set;
}
public static bool IsValid
{
get => !string.IsNullOrEmpty(Server) && !string.IsNullOrEmpty(Model);
@ -113,14 +125,14 @@ namespace SourceGit.Models
try
{
var task = client.PostAsync(Server, req, cancellation);
task.Wait();
task.Wait(cancellation);
var rsp = task.Result;
if (!rsp.IsSuccessStatusCode)
throw new Exception($"AI service returns error code {rsp.StatusCode}");
var reader = rsp.Content.ReadAsStringAsync(cancellation);
reader.Wait();
reader.Wait(cancellation);
return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse);
}

View file

@ -169,6 +169,32 @@ namespace SourceGit.Models
return rule;
}
public IssueTrackerRule AddGitLabIssueTracker(string repoURL)
{
var rule = new IssueTrackerRule()
{
Name = "GitLab ISSUE",
RegexString = "#(\\d+)",
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://gitlab.com/username/repository/-/issues/$1" : $"{repoURL}/-/issues/$1",
};
IssueTrackerRules.Add(rule);
return rule;
}
public IssueTrackerRule AddGitLabMergeRequestTracker(string repoURL)
{
var rule = new IssueTrackerRule()
{
Name = "GitLab MR",
RegexString = "!(\\d+)",
URLTemplate = string.IsNullOrEmpty(repoURL) ? "https://gitlab.com/username/repository/-/merge_requests/$1" : $"{repoURL}/-/merge_requests/$1",
};
IssueTrackerRules.Add(rule);
return rule;
}
public void RemoveIssueTracker(IssueTrackerRule rule)
{
if (rule != null)

View file

@ -147,6 +147,8 @@
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">ISSUE TRACKER</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGithub" xml:space="preserve">Add Sample Github Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleJira" xml:space="preserve">Add Sample Jira Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGitLabIssue" xml:space="preserve">Add Sample GitLab Issue Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGitLabMergeRequest" xml:space="preserve">Add Sample GitLab Merge Request Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.NewRule" xml:space="preserve">New Rule</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Regex" xml:space="preserve">Issue Regex Expression:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">Rule Name:</x:String>
@ -400,9 +402,11 @@
<x:String x:Key="Text.Period.YearsAgo" xml:space="preserve">{0} years ago</x:String>
<x:String x:Key="Text.Preference" xml:space="preserve">Preference</x:String>
<x:String x:Key="Text.Preference.AI" xml:space="preserve">OPEN AI</x:String>
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">Server</x:String>
<x:String x:Key="Text.Preference.AI.AnalyzeDiffPrompt" xml:space="preserve">Analyze Diff Prompt</x:String>
<x:String x:Key="Text.Preference.AI.ApiKey" xml:space="preserve">API Key</x:String>
<x:String x:Key="Text.Preference.AI.GenerateSubjectPrompt" xml:space="preserve">Generate Subject Prompt</x:String>
<x:String x:Key="Text.Preference.AI.Model" xml:space="preserve">Model</x:String>
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">Server</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">APPEARANCE</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">Default Font</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFontSize" xml:space="preserve">Default Font Size</x:String>
@ -621,7 +625,7 @@
<x:String x:Key="Text.Welcome.OpenAllInNode" xml:space="preserve">Open All Repositories</x:String>
<x:String x:Key="Text.Welcome.OpenOrInit" xml:space="preserve">Open Repository</x:String>
<x:String x:Key="Text.Welcome.OpenTerminal" xml:space="preserve">Open Terminal</x:String>
<x:String x:Key="Text.Welcome.ScanDefaultCloneDir" xml:space="preserve">Rescan Repositories in Default Clone Dir</x:String>
<x:String x:Key="Text.Welcome.ScanDefaultCloneDir" xml:space="preserve">Rescan Repositories in Default Clone Dir</x:String>
<x:String x:Key="Text.Welcome.Search" xml:space="preserve">Search Repositories...</x:String>
<x:String x:Key="Text.Welcome.Sort" xml:space="preserve">Sort</x:String>
<x:String x:Key="Text.WorkingCopy" xml:space="preserve">Changes</x:String>

View file

@ -150,6 +150,8 @@
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">ISSUE追踪</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGithub" xml:space="preserve">新增匹配Github Issue规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleJira" xml:space="preserve">新增匹配Jira规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGitLabIssue" xml:space="preserve">新增匹配GitLab议题规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGitLabMergeRequest" xml:space="preserve">新增匹配GitLab合并请求规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.NewRule" xml:space="preserve">新增自定义规则</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Regex" xml:space="preserve">匹配ISSUE的正则表达式 </x:String>
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">规则名 </x:String>

View file

@ -150,6 +150,8 @@
<x:String x:Key="Text.Configure.IssueTracker" xml:space="preserve">Issue 追蹤</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGithub" xml:space="preserve">新增符合 GitHub Issue 規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleJira" xml:space="preserve">新增符合 Jira 規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGitLabIssue" xml:space="preserve">新增符合 GitLab 議題規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.AddSampleGitLabMergeRequest" xml:space="preserve">新增符合 GitLab 合併請求規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.NewRule" xml:space="preserve">新增自訂規則</x:String>
<x:String x:Key="Text.Configure.IssueTracker.Regex" xml:space="preserve">符合 Issue 的正則表達式:</x:String>
<x:String x:Key="Text.Configure.IssueTracker.RuleName" xml:space="preserve">規則名稱:</x:String>

View file

@ -45,7 +45,7 @@
<PackageReference Include="Avalonia.Diagnostics" Version="11.1.4" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc3.3" />
<PackageReference Include="TextMateSharp" Version="1.0.63" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.63" />

View file

@ -194,9 +194,9 @@ namespace SourceGit.ViewModels
return -1;
if (l.Backend is Models.Branch)
return r.Backend is Models.Branch ? string.Compare(l.Name, r.Name, StringComparison.Ordinal) : 1;
return r.Backend is Models.Branch ? Models.NumericSort.Compare(l.Name, r.Name) : 1;
return r.Backend is Models.Branch ? -1 : string.Compare(l.Name, r.Name, StringComparison.Ordinal);
return r.Backend is Models.Branch ? -1 : Models.NumericSort.Compare(l.Name, r.Name);
});
foreach (var node in nodes)

View file

@ -114,9 +114,9 @@ namespace SourceGit.ViewModels
nodes.Sort((l, r) =>
{
if (l.IsFolder)
return r.IsFolder ? string.Compare(l.FullPath, r.FullPath, StringComparison.Ordinal) : -1;
return r.IsFolder ? 1 : string.Compare(l.FullPath, r.FullPath, StringComparison.Ordinal);
if (l.IsFolder == r.IsFolder)
return Models.NumericSort.Compare(l.FullPath, r.FullPath);
return l.IsFolder ? -1 : 1;
});
}

View file

@ -25,6 +25,7 @@ namespace SourceGit.ViewModels
_instance.PrepareGit();
_instance.PrepareShellOrTerminal();
_instance.PrepareWorkspaces();
_instance.PrepareOpenAIPrompt();
return _instance;
}
@ -315,6 +316,32 @@ namespace SourceGit.ViewModels
}
}
public string OpenAIAnalyzeDiffPrompt
{
get => Models.OpenAI.AnalyzeDiffPrompt;
set
{
if (value != Models.OpenAI.AnalyzeDiffPrompt)
{
Models.OpenAI.AnalyzeDiffPrompt = value;
OnPropertyChanged();
}
}
}
public string OpenAIGenerateSubjectPrompt
{
get => Models.OpenAI.GenerateSubjectPrompt;
set
{
if (value != Models.OpenAI.GenerateSubjectPrompt)
{
Models.OpenAI.GenerateSubjectPrompt = value;
OnPropertyChanged();
}
}
}
public uint StatisticsSampleColor
{
get => _statisticsSampleColor;
@ -527,6 +554,45 @@ namespace SourceGit.ViewModels
}
}
private void PrepareOpenAIPrompt()
{
if (string.IsNullOrEmpty(Models.OpenAI.AnalyzeDiffPrompt))
{
Models.OpenAI.AnalyzeDiffPrompt = """
You are an expert developer specialist in creating commits.
Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:
- Do not use any code snippets, imports, file routes or bullets points.
- Do not mention the route of file that has been change.
- Write clear, concise, and descriptive messages that explain the MAIN GOAL made of the changes.
- Use the present tense and active voice in the message, for example, "Fix bug" instead of "Fixed bug.".
- Use the imperative mood, which gives the message a sense of command, e.g. "Add feature" instead of "Added feature".
- Avoid using general terms like "update" or "change", be specific about what was updated or changed.
- Avoid using terms like "The main goal of", just output directly the summary in plain text
""";
}
if (string.IsNullOrEmpty(Models.OpenAI.GenerateSubjectPrompt))
{
Models.OpenAI.GenerateSubjectPrompt = """
You are an expert developer specialist in creating commits messages.
Your only goal is to retrieve a single commit message.
Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:
- Assign the commit {type} according to the next conditions:
feat: Only when adding a new feature.
fix: When fixing a bug.
docs: When updating documentation.
style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic.
test: When adding or updating tests.
chore: When making changes to the build process or auxiliary tools and libraries.
revert: When undoing a previous commit.
refactor: When restructuring code without changing its external behavior, or is any of the other refactor types.
- Do not add any issues numeration, explain your output nor introduce your answer.
- Output directly only one commit message in plain text with the next format: {type}: {commit_message}.
- Be as concise as possible, keep the message under 50 characters.
""";
}
}
private RepositoryNode FindNodeRecursive(string id, List<RepositoryNode> collection)
{
foreach (var node in collection)

View file

@ -172,6 +172,34 @@ namespace SourceGit.ViewModels
SelectedIssueTrackerRule = _repo.Settings.AddJiraIssueTracker();
}
public void AddSampleGitLabIssueTracker()
{
foreach (var remote in _repo.Remotes)
{
if (remote.TryGetVisitURL(out string url))
{
SelectedIssueTrackerRule = _repo.Settings.AddGitLabIssueTracker(url);
return;
}
}
SelectedIssueTrackerRule = _repo.Settings.AddGitLabIssueTracker(null);
}
public void AddSampleGitLabMergeRequestTracker()
{
foreach (var remote in _repo.Remotes)
{
if (remote.TryGetVisitURL(out string url))
{
SelectedIssueTrackerRule = _repo.Settings.AddGitLabMergeRequestTracker(url);
return;
}
}
SelectedIssueTrackerRule = _repo.Settings.AddGitLabMergeRequestTracker(null);
}
public void NewIssueTracker()
{
SelectedIssueTrackerRule = _repo.Settings.AddNewIssueTracker();

View file

@ -52,7 +52,7 @@
<TabItem.Header>
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preference.General}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.General.Locale}"
HorizontalAlignment="Right"
@ -236,7 +236,7 @@
Margin="0,0,16,0"/>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal">
<TextBlock Margin="0,0,8,0"
Text="{Binding #ThisControl.GitVersion}"
Text="{Binding #ThisControl.GitVersion}"
IsVisible="{Binding #ThisControl.GitVersion, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Border Background="Transparent"
@ -368,7 +368,7 @@
<ColumnDefinition Width="Auto" SharedSizeGroup="IntegrationLabel"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.Shell.Type}"
HorizontalAlignment="Right"
@ -405,7 +405,7 @@
</Button>
</TextBox.InnerRightContent>
</TextBox>
</Grid>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,24,0,0">
<Path Width="12" Height="12" Data="{StaticResource Icons.Diff}"/>
@ -417,7 +417,7 @@
<ColumnDefinition Width="Auto" SharedSizeGroup="IntegrationLabel"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.DiffMerge.Type}"
HorizontalAlignment="Right"
@ -465,12 +465,12 @@
<TextBlock Classes="bold" Margin="4,0,0,0" Text="{DynamicResource Text.Preference.AI}"/>
</StackPanel>
<Rectangle Margin="0,8" Fill="{DynamicResource Brush.Border2}" Height=".6" HorizontalAlignment="Stretch"/>
<Grid Margin="8,0,0,0" RowDefinitions="32,32,32">
<Grid Margin="8,0,0,0" RowDefinitions="32,32,32,128,128">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="IntegrationLabel"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.Server}"
HorizontalAlignment="Right"
@ -497,10 +497,34 @@
Height="28"
CornerRadius="3"
Text="{Binding OpenAIApiKey, Mode=TwoWay}"/>
<TextBlock Grid.Row="3" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.AnalyzeDiffPrompt}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="3" Grid.Column="1"
Height="120"
CornerRadius="3"
VerticalContentAlignment="Top"
Text="{Binding OpenAIAnalyzeDiffPrompt, Mode=TwoWay}"
AcceptsReturn="true"
TextWrapping="Wrap"/>
<TextBlock Grid.Row="4" Grid.Column="0"
Text="{DynamicResource Text.Preference.AI.GenerateSubjectPrompt}"
HorizontalAlignment="Right"
Margin="0,0,16,0"/>
<TextBox Grid.Row="4" Grid.Column="1"
Height="120"
CornerRadius="3"
VerticalContentAlignment="Top"
Text="{Binding OpenAIGenerateSubjectPrompt, Mode=TwoWay}"
AcceptsReturn="true"
TextWrapping="Wrap"/>
</Grid>
</StackPanel>
</StackPanel>
</TabItem>
</TabControl>
</Border>
</Border>
</Grid>
</v:ChromelessWindow>

View file

@ -287,6 +287,8 @@
<MenuItem Header="-"/>
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.AddSampleGithub}" Command="{Binding AddSampleGithubIssueTracker}"/>
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.AddSampleJira}" Command="{Binding AddSampleJiraIssueTracker}"/>
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.AddSampleGitLabIssue}" Command="{Binding AddSampleGitLabIssueTracker}"/>
<MenuItem Header="{DynamicResource Text.Configure.IssueTracker.AddSampleGitLabMergeRequest}" Command="{Binding AddSampleGitLabMergeRequestTracker}"/>
</MenuFlyout>
</Button.Flyout>
<Path Width="14" Height="14" Data="{StaticResource Icons.Plus}"/>

View file

@ -284,7 +284,7 @@ namespace SourceGit.Views
node.Children.Sort((l, r) =>
{
if (l.IsFolder == r.IsFolder)
return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
return Models.NumericSort.Compare(l.Name, r.Name);
return l.IsFolder ? -1 : 1;
});