Skip to content

Commit 0bdee96

Browse files
authored
Merge pull request #26 from semantic-developer/feature/sd-28
Add offline catalog fallback and slash undo support
2 parents 280d421 + c17c8ba commit 0bdee96

File tree

9 files changed

+1147
-24
lines changed

9 files changed

+1147
-24
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ Semantic Developer can drive the Codex CLI from Linux, macOS, or Windows. On Win
7272
4. “CLI Settings” lets you change:
7373
- Profile (from Codex `config.toml`) — passed via `-c profile=<name>`
7474
- `config.toml` path: `$CODEX_HOME/config.toml` (defaults to `~/.codex/config.toml`)
75+
- Default model & reasoning effort
76+
- Before a session starts, the picker loads from `SemanticDeveloper/SemanticDeveloper/models.json` so you can choose models offline. Keep this file updated as Codex releases new entries.
77+
- Once a session is running the dialog refreshes with the live catalog. Configured profiles from `config.toml` are appended and marked with an asterisk; selecting a profile disables the reasoning picker and lets the profile decide the model/effort.
7578
- Verbose logging (show suppressed output)
7679
- Enable MCP support (loads MCP servers from your JSON config and passes them directly to Codex)
7780
- MCP config locations:

SemanticDeveloper/SemanticDeveloper/MainWindow.axaml.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,9 @@ private async void OnCloseTabClick(object? sender, RoutedEventArgs e)
284284
ShowMcpResultsInLog = source.ShowMcpResultsInLog,
285285
ShowMcpResultsOnlyWhenNoEdits = source.ShowMcpResultsOnlyWhenNoEdits,
286286
SelectedProfile = source.SelectedProfile,
287-
UseWsl = source.UseWsl
287+
UseWsl = source.UseWsl,
288+
SelectedModelId = source.SelectedModelId,
289+
SelectedReasoningEffort = source.SelectedReasoningEffort
288290
};
289291

290292
public class SessionTab : INotifyPropertyChanged

SemanticDeveloper/SemanticDeveloper/Models/AppSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ public class AppSettings
1212
public bool ShowMcpResultsOnlyWhenNoEdits { get; set; } = true;
1313
public string SelectedProfile { get; set; } = string.Empty;
1414
public bool UseWsl { get; set; } = false;
15+
public string SelectedModelId { get; set; } = string.Empty;
16+
public string SelectedReasoningEffort { get; set; } = string.Empty;
1517
}

SemanticDeveloper/SemanticDeveloper/SemanticDeveloper.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,11 @@
4444
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
4545
</Content>
4646
</ItemGroup>
47+
48+
<ItemGroup>
49+
<Content Include="models.json">
50+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
51+
</Content>
52+
</ItemGroup>
4753
<!-- MCP mux removed; servers are loaded directly by Codex via config flags. -->
4854
</Project>

SemanticDeveloper/SemanticDeveloper/Services/SettingsService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public static AppSettings Load()
4343
settings.UseWsl = false;
4444
}
4545
catch { }
46+
settings.SelectedModelId ??= string.Empty;
47+
settings.SelectedReasoningEffort ??= string.Empty;
4648
if (!OperatingSystem.IsWindows())
4749
settings.UseWsl = false;
4850
_loadedPath = path;
@@ -62,6 +64,8 @@ public static void Save(AppSettings settings)
6264
{
6365
if (!OperatingSystem.IsWindows())
6466
settings.UseWsl = false;
67+
settings.SelectedModelId ??= string.Empty;
68+
settings.SelectedReasoningEffort ??= string.Empty;
6569
var path = _loadedPath ?? FilePathApp;
6670
var dir = Path.GetDirectoryName(path)!;
6771
Directory.CreateDirectory(dir);

SemanticDeveloper/SemanticDeveloper/Views/CliSettingsDialog.axaml

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,43 @@
1515
<TextBlock Text="Codex command" Margin="0,0,0,4"/>
1616
<TextBox Text="{Binding Command, Mode=TwoWay}"
1717
Watermark="codex or full path to codex.exe"
18-
MaxWidth="550"/>
19-
<TextBlock Margin="0,4,0,0" Foreground="#888" FontSize="12"
18+
HorizontalAlignment="Stretch"
19+
Margin="0,0,24,0"/>
20+
<TextBlock Margin="0,4,24,0" Foreground="#888" FontSize="12"
2021
Text="Set to the full path (e.g., C:\Users\myuser\AppData\Roaming\npm\codex) if the CLI is not on PATH."
21-
MaxWidth="550" TextWrapping="Wrap"/>
22+
TextWrapping="Wrap"/>
23+
</StackPanel>
24+
<StackPanel>
25+
<TextBlock Text="Model" Margin="0,4,0,4"/>
26+
<ComboBox x:Name="ModelComboBox"
27+
HorizontalAlignment="Stretch"
28+
Margin="0,0,24,0"
29+
SelectionChanged="OnModelSelectionChanged">
30+
<ComboBox.ItemTemplate>
31+
<DataTemplate>
32+
<StackPanel Spacing="2">
33+
<TextBlock Text="{Binding DisplayName}" FontWeight="SemiBold"/>
34+
<TextBlock Text="{Binding Description}" Foreground="#888" FontSize="12" TextWrapping="Wrap"/>
35+
</StackPanel>
36+
</DataTemplate>
37+
</ComboBox.ItemTemplate>
38+
</ComboBox>
2239
</StackPanel>
2340
<StackPanel>
24-
<TextBlock Text="Profile" Margin="0,0,0,4"/>
25-
<ComboBox ItemsSource="{Binding Profiles}" SelectedItem="{Binding SelectedProfile, Mode=TwoWay}" MaxWidth="550"/>
26-
<TextBlock Margin="0,4,0,0" Foreground="#888" FontSize="12" Text="Profiles from config.toml (e.g., ~/.codex/config.toml)." MaxWidth="550" TextWrapping="Wrap"/>
41+
<TextBlock Text="Reasoning effort" Margin="0,0,0,4"/>
42+
<ComboBox x:Name="ReasoningComboBox"
43+
HorizontalAlignment="Stretch"
44+
Margin="0,0,24,0"
45+
SelectionChanged="OnReasoningSelectionChanged">
46+
<ComboBox.ItemTemplate>
47+
<DataTemplate>
48+
<StackPanel Spacing="2">
49+
<TextBlock Text="{Binding DisplayLabel}"/>
50+
<TextBlock Text="{Binding Description}" Foreground="#888" FontSize="11" TextWrapping="Wrap"/>
51+
</StackPanel>
52+
</DataTemplate>
53+
</ComboBox.ItemTemplate>
54+
</ComboBox>
2755
</StackPanel>
2856
<CheckBox Content="Verbose logging (show suppressed content)" IsChecked="{Binding VerboseLoggingEnabled, Mode=TwoWay}"/>
2957
<StackPanel>

SemanticDeveloper/SemanticDeveloper/Views/CliSettingsDialog.axaml.cs

Lines changed: 190 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using Avalonia.Controls;
35
using Avalonia.Interactivity;
46

@@ -14,20 +16,206 @@ public class CliSettings
1416
public bool AllowNetworkAccess { get; set; } = true;
1517
public bool ShowMcpResultsInLog { get; set; } = true;
1618
public bool ShowMcpResultsOnlyWhenNoEdits { get; set; } = true;
17-
public System.Collections.Generic.List<string> Profiles { get; set; } = new();
1819
public string SelectedProfile { get; set; } = string.Empty;
1920
public bool UseWsl { get; set; } = false;
2021
public bool CanUseWsl { get; set; } = OperatingSystem.IsWindows();
22+
public List<CliModelOption> Models { get; set; } = new();
23+
public string SelectedModelId { get; set; } = string.Empty;
24+
public string SelectedReasoningEffort { get; set; } = string.Empty;
25+
}
26+
27+
public class CliModelOption
28+
{
29+
public string Id { get; set; } = string.Empty;
30+
public string Model { get; set; } = string.Empty;
31+
public string DisplayName { get; set; } = string.Empty;
32+
public string Description { get; set; } = string.Empty;
33+
public string DefaultReasoningEffort { get; set; } = string.Empty;
34+
public List<CliReasoningOption> ReasoningOptions { get; set; } = new();
35+
public bool IsProfile { get; set; }
36+
public string ProfileName { get; set; } = string.Empty;
37+
38+
public override string ToString() => DisplayName;
39+
}
40+
41+
public class CliReasoningOption
42+
{
43+
public string Effort { get; set; } = string.Empty;
44+
public string DisplayLabel { get; set; } = string.Empty;
45+
public string Description { get; set; } = string.Empty;
46+
47+
public override string ToString() => DisplayLabel;
2148
}
2249

2350
public partial class CliSettingsDialog : Window
2451
{
52+
private bool _initializingModelSelection;
53+
2554
public CliSettingsDialog()
2655
{
2756
InitializeComponent();
2857
}
2958

30-
private CliSettings ViewModel => (DataContext as CliSettings) ?? new CliSettings();
59+
private CliSettings? ViewModel => DataContext as CliSettings;
60+
61+
private ComboBox? ModelComboControl => this.FindControl<ComboBox>("ModelComboBox");
62+
private ComboBox? ReasoningComboControl => this.FindControl<ComboBox>("ReasoningComboBox");
63+
64+
protected override void OnOpened(EventArgs e)
65+
{
66+
base.OnOpened(e);
67+
PopulateModelControls();
68+
}
69+
70+
private void PopulateModelControls()
71+
{
72+
var vm = ViewModel;
73+
var modelCombo = ModelComboControl;
74+
if (vm is null || modelCombo is null)
75+
return;
76+
77+
_initializingModelSelection = true;
78+
try
79+
{
80+
modelCombo.ItemsSource = vm.Models;
81+
82+
CliModelOption? selection = null;
83+
if (!string.IsNullOrWhiteSpace(vm.SelectedProfile))
84+
{
85+
selection = vm.Models.FirstOrDefault(m => m.IsProfile && string.Equals(m.ProfileName, vm.SelectedProfile, StringComparison.OrdinalIgnoreCase));
86+
}
87+
88+
if (selection is null && !string.IsNullOrWhiteSpace(vm.SelectedModelId))
89+
{
90+
selection = vm.Models.FirstOrDefault(m => !m.IsProfile && string.Equals(m.Id, vm.SelectedModelId, StringComparison.OrdinalIgnoreCase));
91+
}
92+
93+
if (selection is null && vm.Models.Count > 0)
94+
{
95+
selection = vm.Models[0];
96+
if (selection.IsProfile)
97+
{
98+
vm.SelectedProfile = selection.ProfileName;
99+
vm.SelectedModelId = string.Empty;
100+
}
101+
else
102+
{
103+
vm.SelectedModelId = selection.Id;
104+
vm.SelectedProfile = string.Empty;
105+
}
106+
}
107+
108+
modelCombo.SelectedItem = selection;
109+
if (selection != null)
110+
{
111+
if (selection.IsProfile)
112+
{
113+
vm.SelectedProfile = selection.ProfileName;
114+
vm.SelectedModelId = string.Empty;
115+
}
116+
else
117+
{
118+
vm.SelectedProfile = string.Empty;
119+
vm.SelectedModelId = selection.Id;
120+
}
121+
}
122+
UpdateReasoningOptionsForModel(selection, vm.SelectedReasoningEffort);
123+
}
124+
finally
125+
{
126+
_initializingModelSelection = false;
127+
}
128+
}
129+
130+
private void UpdateReasoningOptionsForModel(CliModelOption? model, string? desiredEffort)
131+
{
132+
var vm = ViewModel;
133+
var effortCombo = ReasoningComboControl;
134+
if (vm is null || effortCombo is null)
135+
return;
136+
137+
effortCombo.ItemsSource = null;
138+
if (model is null)
139+
{
140+
effortCombo.ItemsSource = null;
141+
effortCombo.IsEnabled = false;
142+
vm.SelectedReasoningEffort = string.Empty;
143+
return;
144+
}
145+
146+
if (model.IsProfile)
147+
{
148+
effortCombo.ItemsSource = null;
149+
effortCombo.IsEnabled = false;
150+
vm.SelectedReasoningEffort = string.Empty;
151+
return;
152+
}
153+
154+
effortCombo.IsEnabled = true;
155+
effortCombo.ItemsSource = model.ReasoningOptions;
156+
157+
var targetEffort = desiredEffort;
158+
if (string.IsNullOrWhiteSpace(targetEffort) ||
159+
!model.ReasoningOptions.Any(o => string.Equals(o.Effort, targetEffort, StringComparison.OrdinalIgnoreCase)))
160+
{
161+
targetEffort = model.DefaultReasoningEffort ?? model.ReasoningOptions.FirstOrDefault()?.Effort ?? string.Empty;
162+
}
163+
164+
CliReasoningOption? selectedOption = null;
165+
if (!string.IsNullOrWhiteSpace(targetEffort))
166+
{
167+
selectedOption = model.ReasoningOptions.FirstOrDefault(o => string.Equals(o.Effort, targetEffort, StringComparison.OrdinalIgnoreCase));
168+
}
169+
170+
if (selectedOption is null && model.ReasoningOptions.Count > 0)
171+
{
172+
selectedOption = model.ReasoningOptions[0];
173+
}
174+
175+
effortCombo.SelectedItem = selectedOption;
176+
vm.SelectedReasoningEffort = selectedOption?.Effort ?? string.Empty;
177+
}
178+
179+
private void OnModelSelectionChanged(object? sender, SelectionChangedEventArgs e)
180+
{
181+
if (_initializingModelSelection)
182+
return;
183+
184+
var vm = ViewModel;
185+
var selected = ModelComboControl?.SelectedItem as CliModelOption;
186+
if (vm is null || selected is null)
187+
return;
188+
189+
if (selected.IsProfile)
190+
{
191+
vm.SelectedProfile = selected.ProfileName;
192+
vm.SelectedModelId = string.Empty;
193+
UpdateReasoningOptionsForModel(selected, null);
194+
}
195+
else
196+
{
197+
vm.SelectedProfile = string.Empty;
198+
vm.SelectedModelId = selected.Id;
199+
UpdateReasoningOptionsForModel(selected, vm.SelectedReasoningEffort);
200+
}
201+
}
202+
203+
private void OnReasoningSelectionChanged(object? sender, SelectionChangedEventArgs e)
204+
{
205+
if (_initializingModelSelection)
206+
return;
207+
208+
var vm = ViewModel;
209+
var effortCombo = ReasoningComboControl;
210+
if (vm is null || effortCombo is null || effortCombo.IsEnabled == false)
211+
return;
212+
213+
var option = effortCombo.SelectedItem as CliReasoningOption;
214+
if (option is null)
215+
return;
216+
217+
vm.SelectedReasoningEffort = option.Effort;
218+
}
31219

32220
private void OnSave(object? sender, RoutedEventArgs e)
33221
=> Close(ViewModel);

0 commit comments

Comments
 (0)