feat: prioritize finish meeting tray action
PR and Push Build/Test / build-and-test (push) Successful in 11m8s

This commit is contained in:
2026-08-04 09:55:18 +02:00
parent 5d0ae84426
commit 2f12a96688
8 changed files with 220 additions and 30 deletions
+43 -16
View File
@@ -60,27 +60,54 @@ public sealed class TaskbarIconTests
}
[Fact]
public void RecordingMenuOffersStopAbortAndOtherProfileSwitches()
public void RecordingMenuPrioritizesFinishMeetingInDedicatedSection()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(isRecording: true, state: RecordingProcessState.Recording, profile: "default"),
[Profile("default", "Ctrl+Alt+M"), Profile("english", "Ctrl+Alt+L"), Profile("french", "Ctrl+Alt+F")]);
[Profile("default", "Ctrl+Alt+M"), Profile("english", "Ctrl+Alt+L")],
[new MicrophoneDevice("integrated", "integrated microphone")],
"integrated");
Assert.Equal(RecordingProcessState.Recording, menu.State);
Assert.Contains(menu.Items, item => item.Action == MeetingTaskbarAction.StopRecording);
Assert.Contains(menu.Items, item => item.Action == MeetingTaskbarAction.AbortRecording);
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.SwitchProfile &&
item.ProfileName == "english" &&
item.Text == "Switch to english\tCtrl+Alt+L");
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.SwitchProfile &&
item.ProfileName == "french" &&
item.Text == "Switch to french\tCtrl+Alt+F");
Assert.DoesNotContain(menu.Items, item =>
item.Action == MeetingTaskbarAction.SwitchProfile &&
item.ProfileName == "default");
Assert.DoesNotContain(menu.Items, item => item.Action == MeetingTaskbarAction.StartRecording);
Assert.Collection(
menu.Items,
item =>
{
Assert.Equal("Open agent", item.Text);
Assert.Equal(MeetingTaskbarAction.EditRules, item.Action);
Assert.False(item.StartsSection);
},
item =>
{
Assert.Equal("Finish meeting", item.Text);
Assert.Equal(MeetingTaskbarAction.StopRecording, item.Action);
Assert.True(item.StartsSection);
},
item =>
{
Assert.Equal("Microphone", item.Text);
Assert.Equal(MeetingTaskbarAction.OpenSubmenu, item.Action);
Assert.True(item.StartsSection);
},
item =>
{
Assert.Equal("Cancel meeting recording and discard", item.Text);
Assert.Equal(MeetingTaskbarAction.AbortRecording, item.Action);
Assert.False(item.StartsSection);
},
item =>
{
Assert.Equal("Switch to english\tCtrl+Alt+L", item.Text);
Assert.Equal(MeetingTaskbarAction.SwitchProfile, item.Action);
Assert.Equal("english", item.ProfileName);
Assert.False(item.StartsSection);
},
item =>
{
Assert.Equal("Exit", item.Text);
Assert.Equal(MeetingTaskbarAction.Exit, item.Action);
Assert.True(item.StartsSection);
});
}
[Fact]
+31 -9
View File
@@ -26,7 +26,8 @@ public sealed record MeetingTaskbarMenuItem(
string? ProfileName = null,
string? MicrophoneDeviceId = null,
bool IsChecked = false,
IReadOnlyList<MeetingTaskbarMenuItem>? Items = null);
IReadOnlyList<MeetingTaskbarMenuItem>? Items = null,
bool StartsSection = false);
public static class MeetingTaskbarMenuBuilder
{
@@ -41,23 +42,29 @@ public static class MeetingTaskbarMenuBuilder
new("Open agent", MeetingTaskbarAction.EditRules)
};
if (status.IsRecording)
{
items.Add(new MeetingTaskbarMenuItem(
"Finish meeting",
MeetingTaskbarAction.StopRecording,
StartsSection: true));
}
var secondaryControls = new List<MeetingTaskbarMenuItem>();
if (microphones is { Count: > 0 })
{
items.Add(BuildMicrophoneMenu(microphones, currentMicrophoneDeviceId));
secondaryControls.Add(BuildMicrophoneMenu(microphones, currentMicrophoneDeviceId));
}
if (status.IsRecording)
{
items.Add(new MeetingTaskbarMenuItem(
"Stop meeting recording and transcribe",
MeetingTaskbarAction.StopRecording));
items.Add(new MeetingTaskbarMenuItem(
secondaryControls.Add(new MeetingTaskbarMenuItem(
"Cancel meeting recording and discard",
MeetingTaskbarAction.AbortRecording));
foreach (var profile in launchProfiles.Where(profile => !IsActiveProfile(profile, status)))
{
items.Add(new MeetingTaskbarMenuItem(
secondaryControls.Add(new MeetingTaskbarMenuItem(
AppendHotkey($"Switch to {profile.Name}", profile.Options.Hotkey.Toggle),
MeetingTaskbarAction.SwitchProfile,
profile.Name));
@@ -67,16 +74,18 @@ public static class MeetingTaskbarMenuBuilder
{
foreach (var profile in launchProfiles)
{
items.Add(new MeetingTaskbarMenuItem(
secondaryControls.Add(new MeetingTaskbarMenuItem(
AppendHotkey($"Start meeting recording ({profile.Name})", profile.Options.Hotkey.Toggle),
MeetingTaskbarAction.StartRecording,
profile.Name));
}
}
AddSection(items, secondaryControls);
items.Add(new MeetingTaskbarMenuItem(
"Exit",
MeetingTaskbarAction.Exit));
MeetingTaskbarAction.Exit,
StartsSection: true));
return new MeetingTaskbarMenu(
status.State,
@@ -102,6 +111,19 @@ public static class MeetingTaskbarMenuBuilder
Items: microphoneItems);
}
private static void AddSection(
List<MeetingTaskbarMenuItem> items,
List<MeetingTaskbarMenuItem> section)
{
if (section.Count == 0)
{
return;
}
section[0] = section[0] with { StartsSection = true };
items.AddRange(section);
}
private static string BuildTooltip(RecordingStatus status)
{
return status.State switch
@@ -196,14 +196,12 @@ public sealed class UnoTaskbarIconService : IHostedService, IDisposable
var popupMenu = new PopupMenu();
for (var index = 0; index < menu.Items.Count; index++)
{
if (index == 1 ||
(menu.Items[index].Action == MeetingTaskbarAction.Exit &&
menu.Items[index - 1].Action != MeetingTaskbarAction.EditRules))
var menuItem = menu.Items[index];
if (index > 0 && menuItem.StartsSection)
{
popupMenu.Items.Add(new PopupMenuSeparator());
}
var menuItem = menu.Items[index];
popupMenu.Items.Add(BuildPopupItem(menuItem));
}
@@ -290,7 +288,7 @@ public sealed class UnoTaskbarIconService : IHostedService, IDisposable
return string.Join(
"|",
FlattenMenuItems(menu.Items).Select(item =>
$"{item.Action}:{item.ProfileName}:{item.MicrophoneDeviceId}:{item.IsChecked}:{item.Text}"));
$"{item.Action}:{item.ProfileName}:{item.MicrophoneDeviceId}:{item.IsChecked}:{item.StartsSection}:{item.Text}"));
}
private static IEnumerable<MeetingTaskbarMenuItem> FlattenMenuItems(
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-04
@@ -0,0 +1,45 @@
## Context
The tray-menu builder currently returns a flat list of semantic actions, while the Windows renderer infers separators from item indexes and the Exit action. During an active recording, the normal stop action is added after the microphone submenu and uses a long implementation-oriented label. This makes the primary meeting-completion action look equivalent to cancel, profile switching, and device selection.
## Goals / Non-Goals
**Goals:**
- Give normal meeting completion the concise label `Finish meeting`.
- Make that action the only item in the section immediately below `Open agent` while recording.
- Keep fine-grained recording controls in a distinct following section.
- Make section boundaries observable in platform-independent menu behavior tests.
**Non-Goals:**
- Change what normal stop, abort, profile switching, or microphone selection does.
- Change idle-menu actions, hotkeys, endpoints, or recording state transitions.
- Add icons, confirmation prompts, or nested submenus.
## Decisions
### Represent section starts in the menu model
Add a section-start flag to `MeetingTaskbarMenuItem`. The Windows renderer will insert a separator before items carrying the flag instead of deriving layout from array indexes and action types.
This keeps layout intent in the platform-independent builder where behavior tests can observe it. Keeping another renderer-only special case was rejected because it would leave the requested prominence untestable without Windows UI automation.
### Build prioritized and fine-grained controls as separate groups
While recording, the builder will add `Open agent`, then `Finish meeting` as a new section, then collect microphone, cancel/discard, and profile-switch actions into a fine-grained group whose first item starts another section. Exit remains the final section.
The action continues to use the existing normal-stop command so transcription, speaker processing, OCR, and summarization semantics do not change.
## Risks / Trade-offs
- **A section flag could produce adjacent separators if assigned carelessly** → The builder marks only the first item of each non-empty group, and the renderer follows those explicit starts.
- **Menu ordering changes while recording** → Limit reordering to the active-recording state; idle and processing actions retain their existing relative order.
## Migration Plan
No configuration or data migration is required. Deploying the updated executable changes only tray-menu presentation. Rollback restores the previous label and grouping.
## Open Questions
None.
@@ -0,0 +1,25 @@
## Why
The active-recording tray menu labels its most important completion action as the verbose `Stop meeting recording and transcribe` and groups it with rarely used controls. Finishing a meeting should be immediately recognizable and visually prioritized during normal use.
## What Changes
- Rename the active-recording stop action to `Finish meeting` without changing its normal stop, transcription, or summary behavior.
- Place `Finish meeting` by itself in the section immediately below `Open agent`.
- Place microphone selection, cancel/discard, and profile-switch controls in a separate lower-priority section.
- Represent tray-menu section boundaries explicitly so ordering and prominence are behavior-testable.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `meeting-recording`: Prioritize the normal meeting completion action in the Windows tray menu with a concise label and dedicated section.
## Impact
- Affects the platform-independent tray-menu model/builder, Windows tray-menu rendering, and taskbar behavior tests.
- Does not change recording lifecycle semantics, hotkeys, endpoints, or generated meeting artifacts.
@@ -0,0 +1,62 @@
## MODIFIED Requirements
### Requirement: Windows taskbar icon controls recording
Meeting Assistant SHALL show a Windows taskbar notification icon when running on Windows.
The taskbar icon SHALL indicate whether the newest meeting process is idle, actively recording, or post-recording processing/summarizing.
When a new meeting is actively recording while an older stopped meeting is still transcribing, recognizing speakers, or summarizing, the taskbar icon SHALL show the new active recording state.
The taskbar icon right-click menu SHALL expose recording controls based on the current state and configured launch profiles.
The taskbar icon right-click menu SHALL expose an Exit action in every recording state.
When Meeting Assistant is idle or only processing older stopped meetings, the menu SHALL allow starting a meeting recording for each configured launch profile.
When a meeting is actively recording, the menu SHALL allow stopping the recording and continuing transcription/summary generation.
During an active recording, the normal stop action SHALL be labeled `Finish meeting` and SHALL be the only action in a dedicated menu section immediately below the `Open agent` section.
During an active recording, microphone selection, cancel/discard, and profile-switch actions SHALL appear in a separate fine-grained controls section below `Finish meeting`.
When a meeting is actively recording, the menu SHALL allow canceling the recording and discarding that run's artifacts.
When a meeting is actively recording, the menu SHALL allow switching to each configured launch profile other than the current active profile.
Selecting Exit while Meeting Assistant is idle SHALL stop the application without an additional confirmation prompt.
Selecting Exit while Meeting Assistant is recording, transcribing, recognizing speakers, or summarizing SHALL show a confirmation dialog before stopping the application.
#### Scenario: Idle tray menu can start configured profiles
- **GIVEN** launch profiles `default` and `english` are configured
- **AND** no meeting recording is active
- **WHEN** the taskbar menu is opened
- **THEN** it offers start recording actions for `default` and `english`
#### Scenario: Recording tray menu prioritizes finishing the meeting
- **GIVEN** launch profiles `default` and `english` are configured
- **AND** a meeting is actively recording with profile `default`
- **WHEN** the taskbar menu is opened
- **THEN** `Finish meeting` is the only action in the section immediately below `Open agent`
- **AND** microphone selection, cancel/discard, and switching to `english` appear in a separate following section
- **AND** the menu does not offer switching to `default`
#### Scenario: Active recording has priority over older summarizing runs
- **GIVEN** an older meeting is still summarizing
- **WHEN** a newer meeting is actively recording
- **THEN** the taskbar icon indicates recording
#### Scenario: Tray menu always exposes Exit
- **GIVEN** Meeting Assistant is running
- **WHEN** the taskbar menu is opened
- **THEN** it offers an Exit action
#### Scenario: Idle Exit stops immediately
- **GIVEN** no recording, transcription, speaker recognition, or summary work is running
- **WHEN** the user selects Exit from the taskbar menu
- **THEN** Meeting Assistant stops the application without an additional confirmation prompt
#### Scenario: In-progress Exit asks for confirmation
- **GIVEN** Meeting Assistant is recording, transcribing, recognizing speakers, or summarizing
- **WHEN** the user selects Exit from the taskbar menu
- **THEN** Meeting Assistant asks for confirmation before stopping the application
@@ -0,0 +1,9 @@
## 1. Tray Menu Behavior
- [x] 1.1 Add a failing behavior test proving that an active recording labels the normal stop action `Finish meeting`, places it alone immediately below `Open agent`, and keeps fine-grained controls in the following section.
- [x] 1.2 Add explicit section metadata to the tray-menu model, reorder the active-recording actions, and render separators from that metadata.
## 2. Verification
- [x] 2.1 Review the touched menu builder and renderer for DRYness, SOLID design, and simplicity while preserving behavior.
- [x] 2.2 Run focused taskbar-menu tests, the Windows application build, the full solution tests, and strict OpenSpec validation.