Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ A cross‑platform desktop UI (Avalonia/.NET 8) for driving the Codex CLI app se
- Allow network access for tools (sets sandbox_policy.network_access=true on turns so MCP tools can reach the network)
- Without API key enabled, the app proactively authenticates with `codex auth login` (falling back to `codex login`) before sessions so your chat/GPT token is used.
5. Need a second workspace or want to keep another Codex stream alive? Hit the **+** button next to the session tabs to spin up a parallel session—tab titles update in real time so you can see whether each workspace is `disconnected`, `thinking…`, or `idle`.
6. Right-click a tab to rename it or use the per-session **Close Tab** button/context menu to shut it down when you are done.

### Directory Guardrails with `AGENTS.md`

Expand Down
2 changes: 1 addition & 1 deletion SemanticDeveloper/Installers/Linux/build_deb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ APP_PROJ="$ROOT/SemanticDeveloper/SemanticDeveloper.csproj"
PUBLISH_DIR="$SCRIPT_DIR/out/publish"
PKG_ROOT="$SCRIPT_DIR/pkgroot"
DIST_DIR="$SCRIPT_DIR/dist"
VERSION="1.0.4"
VERSION="1.0.5"
ARCH="amd64"
if [[ "$RID" == "linux-arm64" ]]; then ARCH="arm64"; fi

Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion SemanticDeveloper/Installers/Windows/SemanticDeveloper.iss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
; Inno Setup script to package SemanticDeveloper
#define AppName "SemanticDeveloper"
#define AppVersion "1.0.1"
#define AppVersion "1.0.5"
#define Publisher "Stainless Designer LLC"
#define URL "https://github.com/stainless-design/semantic-developer"
#ifndef RID
Expand Down
4 changes: 2 additions & 2 deletions SemanticDeveloper/Installers/macOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
<key>CFBundleIdentifier</key>
<string>com.stainlessdesigner.semanticdeveloper</string>
<key>CFBundleVersion</key>
<string>1.0.1</string>
<string>1.0.5</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>1.0.5</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
Expand Down
10 changes: 10 additions & 0 deletions SemanticDeveloper/SemanticDeveloper/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@
</Border>
</ControlTemplate>
</Setter>
<Setter Property="ContextMenu">
<ContextMenu>
<MenuItem Header="Rename..."
CommandParameter="{Binding $parent[TabItem].DataContext}"
Click="OnRenameTabClick"/>
<MenuItem Header="Close"
CommandParameter="{Binding $parent[TabItem].DataContext}"
Click="OnCloseTabClick"/>
</ContextMenu>
</Setter>
</Style>
<Style Selector="TabControl#SessionTabControl > TabItem /template/ Border#border">
<Setter Property="BorderBrush" Value="Transparent"/>
Expand Down
124 changes: 112 additions & 12 deletions SemanticDeveloper/SemanticDeveloper/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,47 @@ private void AddSession(bool applySharedSettings)
{
var title = $"Session {++_sessionCounter}";
var view = new SessionView();
SessionTab? tab = null;

EventHandler closeHandler = null!;
closeHandler = async (_, _) =>
{
if (tab is not null)
await CloseSessionAsync(tab);
};
view.SessionClosedRequested += closeHandler;

if (applySharedSettings)
{
view.ApplySettingsSnapshot(_sharedSettings);
}
var tab = new SessionTab(title, view);
view.PropertyChanged += (_, args) =>

tab = new SessionTab(title, view)
{
CloseRequestedHandler = closeHandler
};

PropertyChangedEventHandler viewHandler = null!;
viewHandler = (_, args) =>
{
if (args.PropertyName == nameof(SessionView.SessionStatus))
{
tab.UpdateHeader();
tab!.UpdateHeader();
}
};
tab.PropertyChanged += (_, args) =>

view.PropertyChanged += viewHandler;
tab.ViewPropertyChangedHandler = viewHandler;

PropertyChangedEventHandler tabHandler = null!;
tabHandler = (_, args) =>
{
if (args.PropertyName == nameof(SessionTab.Header) && ReferenceEquals(SelectedSession, tab))
OnPropertyChanged(nameof(SelectedSessionTitle));
};
tab.PropertyChanged += tabHandler;
tab.TabPropertyChangedHandler = tabHandler;

tab.UpdateHeader();
_sessions.Add(tab);
SelectedSession = tab;
Expand Down Expand Up @@ -174,6 +198,64 @@ private async Task OpenCliSettingsAsync(SessionTab session)
}
}

private async Task CloseSessionAsync(SessionTab tab)
{
if (!_sessions.Contains(tab))
return;

try
{
await tab.View.ShutdownAsync();
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to shut down session: {ex.Message}");
}
finally
{
tab.View.ForceStop();
}

if (tab.CloseRequestedHandler is not null)
tab.View.SessionClosedRequested -= tab.CloseRequestedHandler;
if (tab.ViewPropertyChangedHandler is not null)
tab.View.PropertyChanged -= tab.ViewPropertyChangedHandler;
if (tab.TabPropertyChangedHandler is not null)
tab.PropertyChanged -= tab.TabPropertyChangedHandler;

_sessions.Remove(tab);
if (ReferenceEquals(SelectedSession, tab))
SelectedSession = _sessions.Count > 0 ? _sessions[^1] : null;
}

private async Task PromptRenameSession(SessionTab tab)
{
var dialog = new InputDialog
{
Title = "Rename Session",
Prompt = "Session name:",
Input = tab.DisplayName
};
var result = await dialog.ShowDialog<InputDialogResult?>(this);
var text = result?.Text?.Trim();
if (!string.IsNullOrWhiteSpace(text))
tab.DisplayName = text;
}

private async void OnRenameTabClick(object? sender, RoutedEventArgs e)
{
if (sender is not MenuItem menu || menu.CommandParameter is not SessionTab tab)
return;
await PromptRenameSession(tab);
}

private async void OnCloseTabClick(object? sender, RoutedEventArgs e)
{
if (sender is not MenuItem menu || menu.CommandParameter is not SessionTab tab)
return;
await CloseSessionAsync(tab);
}

private static AppSettings CloneAppSettings(AppSettings source) => new()
{
Command = source.Command,
Expand All @@ -190,17 +272,35 @@ private async Task OpenCliSettingsAsync(SessionTab session)

public class SessionTab : INotifyPropertyChanged
{
private string _displayName;
private string _header = string.Empty;

public SessionTab(string title, SessionView view)
{
Title = title;
View = view;
_header = $"{Title} - {view.SessionStatus}";
_displayName = title;
UpdateHeader();
}

public string Title { get; }
public SessionView View { get; }
public EventHandler? CloseRequestedHandler { get; set; }
public PropertyChangedEventHandler? ViewPropertyChangedHandler { get; set; }
public PropertyChangedEventHandler? TabPropertyChangedHandler { get; set; }

public string DisplayName
{
get => _displayName;
set
{
if (_displayName == value) return;
_displayName = value;
UpdateHeader();
OnPropertyChanged();
}
}

private string _header;
public string Header
{
get => _header;
Expand All @@ -214,11 +314,11 @@ private set

public void UpdateHeader()
{
Header = $"{Title} - {View.SessionStatus}";
}
Header = $"{DisplayName} - {View.SessionStatus}";
}

public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Version>1.0.4</Version>
<Version>1.0.5</Version>
<Copyright>2025 Stainless Designer LLC</Copyright>
</PropertyGroup>

Expand Down
8 changes: 5 additions & 3 deletions SemanticDeveloper/SemanticDeveloper/Views/SessionView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*">
<!-- Top toolbar -->
<Border Background="#2B2B2B" Padding="8,6">
<Grid ColumnDefinitions="Auto,12,*,Auto,8,Auto,8,Auto,12,Auto">
<Grid ColumnDefinitions="Auto,12,*,Auto,8,Auto,8,Auto,12,Auto,8,Auto">
<Button x:Name="SelectWorkspaceButton" Foreground="#E6E6E6" Padding="10,4" Click="OnSelectWorkspaceClick">Select Workspace…</Button>
<Border Grid.Column="1"/>
<TextBlock Grid.Column="2" TextTrimming="CharacterEllipsis" VerticalAlignment="Center" Foreground="#E6E6E6" Margin="0,0,6,0">
Expand Down Expand Up @@ -39,8 +39,10 @@
</MenuFlyout>
</Button.Flyout>
</Button>
<Button Grid.Column="7" x:Name="InitGitButton" Foreground="#E6E6E6" IsVisible="{Binding CanInitGit}" Click="OnGitInitClick">Initialize Git…</Button>
<Button Grid.Column="9" x:Name="OpenInFileManagerButton" Foreground="#E6E6E6" Padding="8,4" Margin="0,0,0,0" Click="OnOpenInFileManagerClick" IsEnabled="{Binding HasWorkspace}">Open in File Manager</Button>
<Button Grid.Column="7" x:Name="InitGitButton" Foreground="#E6E6E6" Padding="8,4" IsVisible="{Binding CanInitGit}" Click="OnGitInitClick">Initialize Git…</Button>
<Button Grid.Column="9" x:Name="OpenInFileManagerButton" Foreground="#E6E6E6" Padding="8,4" Click="OnOpenInFileManagerClick" IsEnabled="{Binding HasWorkspace}">Open in File Manager</Button>
<Border Grid.Column="10"/>
<Button Grid.Column="11" x:Name="CloseSessionButton" Foreground="#E6E6E6" Padding="6,4" Click="OnCloseSessionClick">Close Tab</Button>
</Grid>
</Border>

Expand Down
48 changes: 45 additions & 3 deletions SemanticDeveloper/SemanticDeveloper/Views/SessionView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ namespace SemanticDeveloper.Views;

public partial class SessionView : UserControl, INotifyPropertyChanged
{
public event EventHandler? SessionClosedRequested;

private readonly CodexCliService _cli = new();
private string? _currentModel;
// Auto-approval UI removed; approvals require manual handling
Expand Down Expand Up @@ -113,7 +115,7 @@ public SessionView()
InitializeComponent();

DataContext = this;

McpServers.CollectionChanged += (_, __) =>
{
OnPropertyChanged(nameof(HasMcpServers));
Expand Down Expand Up @@ -2654,13 +2656,18 @@ await SendRequestAsync(
new JObject { ["conversationId"] = _conversationId }
);
}
SetStatusSafe("idle");
}
catch
{
_cli.Stop();
}
finally
{
try { _cli.Stop(); } catch { }
IsCliRunning = false;
SessionStatus = "stopped";
_conversationId = null;
_conversationSubscriptionId = null;
_appServerInitialized = false;
}
}

Expand Down Expand Up @@ -3523,6 +3530,37 @@ private void OnClearLogClick(object? sender, Avalonia.Interactivity.RoutedEventA
try { _logEditor?.ScrollToHome(); } catch { }
}

public async Task ShutdownAsync()
{
try
{
var shutdownTask = InterruptCliAsync();
var completed = await Task.WhenAny(shutdownTask, Task.Delay(TimeSpan.FromSeconds(2)));
if (completed == shutdownTask)
{
await shutdownTask;
}
else
{
ForceStop();
}
}
catch
{
ForceStop();
}
}

public void ForceStop()
{
try { _cli.Stop(); } catch { }
IsCliRunning = false;
SessionStatus = "stopped";
_conversationId = null;
_conversationSubscriptionId = null;
_appServerInitialized = false;
}

private void OnOpenInFileManagerClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (!HasWorkspace || CurrentWorkspacePath is null) return;
Expand All @@ -3547,6 +3585,10 @@ private void OnOpenInFileManagerClick(object? sender, Avalonia.Interactivity.Rou
}
}

private void OnCloseSessionClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SessionClosedRequested?.Invoke(this, EventArgs.Empty);


public async Task<AppSettings?> ShowCliSettingsDialogAsync(AppSettings? seed = null)
{
var window = GetHostWindow();
Expand Down