Skip to content

Commit ac6eafe

Browse files
authored
Merge pull request #22 from semantic-developer/feature/sd-23
added better tab management for multi-session use
2 parents 7cccc99 + a05a91c commit ac6eafe

File tree

10 files changed

+178
-23
lines changed

10 files changed

+178
-23
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ A cross‑platform desktop UI (Avalonia/.NET 8) for driving the Codex CLI app se
4545
- Allow network access for tools (sets sandbox_policy.network_access=true on turns so MCP tools can reach the network)
4646
- 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.
4747
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`.
48+
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.
4849

4950
### Directory Guardrails with `AGENTS.md`
5051

SemanticDeveloper/Installers/Linux/build_deb.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ APP_PROJ="$ROOT/SemanticDeveloper/SemanticDeveloper.csproj"
88
PUBLISH_DIR="$SCRIPT_DIR/out/publish"
99
PKG_ROOT="$SCRIPT_DIR/pkgroot"
1010
DIST_DIR="$SCRIPT_DIR/dist"
11-
VERSION="1.0.4"
11+
VERSION="1.0.5"
1212
ARCH="amd64"
1313
if [[ "$RID" == "linux-arm64" ]]; then ARCH="arm64"; fi
1414

Binary file not shown.

SemanticDeveloper/Installers/Windows/SemanticDeveloper.iss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
; Inno Setup script to package SemanticDeveloper
22
#define AppName "SemanticDeveloper"
3-
#define AppVersion "1.0.1"
3+
#define AppVersion "1.0.5"
44
#define Publisher "Stainless Designer LLC"
55
#define URL "https://github.com/stainless-design/semantic-developer"
66
#ifndef RID

SemanticDeveloper/Installers/macOS/Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
<key>CFBundleIdentifier</key>
1010
<string>com.stainlessdesigner.semanticdeveloper</string>
1111
<key>CFBundleVersion</key>
12-
<string>1.0.1</string>
12+
<string>1.0.5</string>
1313
<key>CFBundleShortVersionString</key>
14-
<string>1.0.1</string>
14+
<string>1.0.5</string>
1515
<key>CFBundlePackageType</key>
1616
<string>APPL</string>
1717
<key>CFBundleExecutable</key>

SemanticDeveloper/SemanticDeveloper/MainWindow.axaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@
7373
</Border>
7474
</ControlTemplate>
7575
</Setter>
76+
<Setter Property="ContextMenu">
77+
<ContextMenu>
78+
<MenuItem Header="Rename..."
79+
CommandParameter="{Binding $parent[TabItem].DataContext}"
80+
Click="OnRenameTabClick"/>
81+
<MenuItem Header="Close"
82+
CommandParameter="{Binding $parent[TabItem].DataContext}"
83+
Click="OnCloseTabClick"/>
84+
</ContextMenu>
85+
</Setter>
7686
</Style>
7787
<Style Selector="TabControl#SessionTabControl > TabItem /template/ Border#border">
7888
<Setter Property="BorderBrush" Value="Transparent"/>

SemanticDeveloper/SemanticDeveloper/MainWindow.axaml.cs

Lines changed: 112 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,47 @@ private void AddSession(bool applySharedSettings)
5757
{
5858
var title = $"Session {++_sessionCounter}";
5959
var view = new SessionView();
60+
SessionTab? tab = null;
61+
62+
EventHandler closeHandler = null!;
63+
closeHandler = async (_, _) =>
64+
{
65+
if (tab is not null)
66+
await CloseSessionAsync(tab);
67+
};
68+
view.SessionClosedRequested += closeHandler;
69+
6070
if (applySharedSettings)
6171
{
6272
view.ApplySettingsSnapshot(_sharedSettings);
6373
}
64-
var tab = new SessionTab(title, view);
65-
view.PropertyChanged += (_, args) =>
74+
75+
tab = new SessionTab(title, view)
76+
{
77+
CloseRequestedHandler = closeHandler
78+
};
79+
80+
PropertyChangedEventHandler viewHandler = null!;
81+
viewHandler = (_, args) =>
6682
{
6783
if (args.PropertyName == nameof(SessionView.SessionStatus))
6884
{
69-
tab.UpdateHeader();
85+
tab!.UpdateHeader();
7086
}
7187
};
72-
tab.PropertyChanged += (_, args) =>
88+
89+
view.PropertyChanged += viewHandler;
90+
tab.ViewPropertyChangedHandler = viewHandler;
91+
92+
PropertyChangedEventHandler tabHandler = null!;
93+
tabHandler = (_, args) =>
7394
{
7495
if (args.PropertyName == nameof(SessionTab.Header) && ReferenceEquals(SelectedSession, tab))
7596
OnPropertyChanged(nameof(SelectedSessionTitle));
7697
};
98+
tab.PropertyChanged += tabHandler;
99+
tab.TabPropertyChangedHandler = tabHandler;
100+
77101
tab.UpdateHeader();
78102
_sessions.Add(tab);
79103
SelectedSession = tab;
@@ -174,6 +198,64 @@ private async Task OpenCliSettingsAsync(SessionTab session)
174198
}
175199
}
176200

201+
private async Task CloseSessionAsync(SessionTab tab)
202+
{
203+
if (!_sessions.Contains(tab))
204+
return;
205+
206+
try
207+
{
208+
await tab.View.ShutdownAsync();
209+
}
210+
catch (Exception ex)
211+
{
212+
Debug.WriteLine($"Failed to shut down session: {ex.Message}");
213+
}
214+
finally
215+
{
216+
tab.View.ForceStop();
217+
}
218+
219+
if (tab.CloseRequestedHandler is not null)
220+
tab.View.SessionClosedRequested -= tab.CloseRequestedHandler;
221+
if (tab.ViewPropertyChangedHandler is not null)
222+
tab.View.PropertyChanged -= tab.ViewPropertyChangedHandler;
223+
if (tab.TabPropertyChangedHandler is not null)
224+
tab.PropertyChanged -= tab.TabPropertyChangedHandler;
225+
226+
_sessions.Remove(tab);
227+
if (ReferenceEquals(SelectedSession, tab))
228+
SelectedSession = _sessions.Count > 0 ? _sessions[^1] : null;
229+
}
230+
231+
private async Task PromptRenameSession(SessionTab tab)
232+
{
233+
var dialog = new InputDialog
234+
{
235+
Title = "Rename Session",
236+
Prompt = "Session name:",
237+
Input = tab.DisplayName
238+
};
239+
var result = await dialog.ShowDialog<InputDialogResult?>(this);
240+
var text = result?.Text?.Trim();
241+
if (!string.IsNullOrWhiteSpace(text))
242+
tab.DisplayName = text;
243+
}
244+
245+
private async void OnRenameTabClick(object? sender, RoutedEventArgs e)
246+
{
247+
if (sender is not MenuItem menu || menu.CommandParameter is not SessionTab tab)
248+
return;
249+
await PromptRenameSession(tab);
250+
}
251+
252+
private async void OnCloseTabClick(object? sender, RoutedEventArgs e)
253+
{
254+
if (sender is not MenuItem menu || menu.CommandParameter is not SessionTab tab)
255+
return;
256+
await CloseSessionAsync(tab);
257+
}
258+
177259
private static AppSettings CloneAppSettings(AppSettings source) => new()
178260
{
179261
Command = source.Command,
@@ -190,17 +272,35 @@ private async Task OpenCliSettingsAsync(SessionTab session)
190272

191273
public class SessionTab : INotifyPropertyChanged
192274
{
275+
private string _displayName;
276+
private string _header = string.Empty;
277+
193278
public SessionTab(string title, SessionView view)
194279
{
195280
Title = title;
196281
View = view;
197-
_header = $"{Title} - {view.SessionStatus}";
282+
_displayName = title;
283+
UpdateHeader();
198284
}
199285

200286
public string Title { get; }
201287
public SessionView View { get; }
288+
public EventHandler? CloseRequestedHandler { get; set; }
289+
public PropertyChangedEventHandler? ViewPropertyChangedHandler { get; set; }
290+
public PropertyChangedEventHandler? TabPropertyChangedHandler { get; set; }
291+
292+
public string DisplayName
293+
{
294+
get => _displayName;
295+
set
296+
{
297+
if (_displayName == value) return;
298+
_displayName = value;
299+
UpdateHeader();
300+
OnPropertyChanged();
301+
}
302+
}
202303

203-
private string _header;
204304
public string Header
205305
{
206306
get => _header;
@@ -214,11 +314,11 @@ private set
214314

215315
public void UpdateHeader()
216316
{
217-
Header = $"{Title} - {View.SessionStatus}";
218-
}
317+
Header = $"{DisplayName} - {View.SessionStatus}";
318+
}
219319

220-
public event PropertyChangedEventHandler? PropertyChanged;
221-
private void OnPropertyChanged([CallerMemberName] string? name = null)
222-
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
223-
}
320+
public event PropertyChangedEventHandler? PropertyChanged;
321+
private void OnPropertyChanged([CallerMemberName] string? name = null)
322+
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
323+
}
224324
}

SemanticDeveloper/SemanticDeveloper/SemanticDeveloper.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
77
<ApplicationManifest>app.manifest</ApplicationManifest>
88
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
9-
<Version>1.0.4</Version>
9+
<Version>1.0.5</Version>
1010
<Copyright>2025 Stainless Designer LLC</Copyright>
1111
</PropertyGroup>
1212

SemanticDeveloper/SemanticDeveloper/Views/SessionView.axaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*">
1212
<!-- Top toolbar -->
1313
<Border Background="#2B2B2B" Padding="8,6">
14-
<Grid ColumnDefinitions="Auto,12,*,Auto,8,Auto,8,Auto,12,Auto">
14+
<Grid ColumnDefinitions="Auto,12,*,Auto,8,Auto,8,Auto,12,Auto,8,Auto">
1515
<Button x:Name="SelectWorkspaceButton" Foreground="#E6E6E6" Padding="10,4" Click="OnSelectWorkspaceClick">Select Workspace…</Button>
1616
<Border Grid.Column="1"/>
1717
<TextBlock Grid.Column="2" TextTrimming="CharacterEllipsis" VerticalAlignment="Center" Foreground="#E6E6E6" Margin="0,0,6,0">
@@ -39,8 +39,10 @@
3939
</MenuFlyout>
4040
</Button.Flyout>
4141
</Button>
42-
<Button Grid.Column="7" x:Name="InitGitButton" Foreground="#E6E6E6" IsVisible="{Binding CanInitGit}" Click="OnGitInitClick">Initialize Git…</Button>
43-
<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>
42+
<Button Grid.Column="7" x:Name="InitGitButton" Foreground="#E6E6E6" Padding="8,4" IsVisible="{Binding CanInitGit}" Click="OnGitInitClick">Initialize Git…</Button>
43+
<Button Grid.Column="9" x:Name="OpenInFileManagerButton" Foreground="#E6E6E6" Padding="8,4" Click="OnOpenInFileManagerClick" IsEnabled="{Binding HasWorkspace}">Open in File Manager</Button>
44+
<Border Grid.Column="10"/>
45+
<Button Grid.Column="11" x:Name="CloseSessionButton" Foreground="#E6E6E6" Padding="6,4" Click="OnCloseSessionClick">Close Tab</Button>
4446
</Grid>
4547
</Border>
4648

SemanticDeveloper/SemanticDeveloper/Views/SessionView.axaml.cs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ namespace SemanticDeveloper.Views;
3636

3737
public partial class SessionView : UserControl, INotifyPropertyChanged
3838
{
39+
public event EventHandler? SessionClosedRequested;
40+
3941
private readonly CodexCliService _cli = new();
4042
private string? _currentModel;
4143
// Auto-approval UI removed; approvals require manual handling
@@ -113,7 +115,7 @@ public SessionView()
113115
InitializeComponent();
114116

115117
DataContext = this;
116-
118+
117119
McpServers.CollectionChanged += (_, __) =>
118120
{
119121
OnPropertyChanged(nameof(HasMcpServers));
@@ -2654,13 +2656,18 @@ await SendRequestAsync(
26542656
new JObject { ["conversationId"] = _conversationId }
26552657
);
26562658
}
2657-
SetStatusSafe("idle");
26582659
}
26592660
catch
26602661
{
2661-
_cli.Stop();
2662+
}
2663+
finally
2664+
{
2665+
try { _cli.Stop(); } catch { }
26622666
IsCliRunning = false;
26632667
SessionStatus = "stopped";
2668+
_conversationId = null;
2669+
_conversationSubscriptionId = null;
2670+
_appServerInitialized = false;
26642671
}
26652672
}
26662673

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

3533+
public async Task ShutdownAsync()
3534+
{
3535+
try
3536+
{
3537+
var shutdownTask = InterruptCliAsync();
3538+
var completed = await Task.WhenAny(shutdownTask, Task.Delay(TimeSpan.FromSeconds(2)));
3539+
if (completed == shutdownTask)
3540+
{
3541+
await shutdownTask;
3542+
}
3543+
else
3544+
{
3545+
ForceStop();
3546+
}
3547+
}
3548+
catch
3549+
{
3550+
ForceStop();
3551+
}
3552+
}
3553+
3554+
public void ForceStop()
3555+
{
3556+
try { _cli.Stop(); } catch { }
3557+
IsCliRunning = false;
3558+
SessionStatus = "stopped";
3559+
_conversationId = null;
3560+
_conversationSubscriptionId = null;
3561+
_appServerInitialized = false;
3562+
}
3563+
35263564
private void OnOpenInFileManagerClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
35273565
{
35283566
if (!HasWorkspace || CurrentWorkspacePath is null) return;
@@ -3547,6 +3585,10 @@ private void OnOpenInFileManagerClick(object? sender, Avalonia.Interactivity.Rou
35473585
}
35483586
}
35493587

3588+
private void OnCloseSessionClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
3589+
=> SessionClosedRequested?.Invoke(this, EventArgs.Empty);
3590+
3591+
35503592
public async Task<AppSettings?> ShowCliSettingsDialogAsync(AppSettings? seed = null)
35513593
{
35523594
var window = GetHostWindow();

0 commit comments

Comments
 (0)