ci: consolidate platform pipelines and test prerequisites on macOS support
PR and Push Build/Test / windows-build-and-test (push) Failing after 38m25s
PR and Push Build/Test / portable-build-and-test (push) Successful in 8m9s
PR and Push Build/Test / macos-native-full (push) Skipped

This commit is contained in:
dh committed 2026-10-06 09:02:19 +02:00
1 parent 1b19b08f2e
commit 728e66dd72
76 files changed
+7477 -239

No files matched your search

@@ -7,10 +7,14 @@ namespace MeetingAssistant.MacOs;
public sealed class MacOsWorkflowRulesEditorWindowService : IWorkflowRulesEditorWindowService
{
private readonly IOptions<MeetingAssistantOptions> options;
private readonly Func<MacOsWorkflowRulesEditorSessions> getSessions;
public MacOsWorkflowRulesEditorWindowService(IOptions<MeetingAssistantOptions> options)
public MacOsWorkflowRulesEditorWindowService(
IOptions<MeetingAssistantOptions> options,
Func<MacOsWorkflowRulesEditorSessions> getSessions)
{
this.options = options;
this.getSessions = getSessions;
}
public void Show()
@@ -31,59 +35,82 @@ public sealed class MacOsWorkflowRulesEditorWindowService : IWorkflowRulesEditor
public void ShowWithPrompt(string initialPrompt)
{
throw new PlatformNotSupportedException(
"Automatic workflow-change conversations require the Windows approval UI. Use the interactive macOS agent instead.");
getSessions().OpenWithPrompt(initialPrompt);
}
}
public sealed class MacOsWorkflowRulesEditorSession
{
private readonly WorkflowRulesEditorChatViewModel viewModel;
private readonly CancellationToken applicationStopping;
private readonly SemaphoreSlim sendLock = new(1, 1);
private MacOsWorkflowRulesEditorChatResponse snapshot = new([], false);
public MacOsWorkflowRulesEditorSession(WorkflowRulesEditorChatViewModel viewModel)
public MacOsWorkflowRulesEditorSession(
WorkflowRulesEditorChatViewModel viewModel,
CancellationToken applicationStopping = default)
{
this.viewModel = viewModel;
this.applicationStopping = applicationStopping;
}
public async Task<MacOsWorkflowRulesEditorChatResponse> SendAsync(
public Task<MacOsWorkflowRulesEditorChatResponse> SendAsync(
string? message,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(message))
{
return Snapshot();
return Task.FromResult(GetSnapshot());
}
await sendLock.WaitAsync(cancellationToken);
// The response connection controls only its wait; accepted agent work belongs to the application.
return SendCoreAsync(message.Trim()).WaitAsync(cancellationToken);
}
public MacOsWorkflowRulesEditorChatResponse GetSnapshot() => Volatile.Read(ref snapshot);
private async Task<MacOsWorkflowRulesEditorChatResponse> SendCoreAsync(string message)
{
await sendLock.WaitAsync(applicationStopping);
try
{
viewModel.Draft = message;
await viewModel.SendAsync(cancellationToken);
return Snapshot();
applicationStopping.ThrowIfCancellationRequested();
var messages = GetSnapshot().Messages
.Append(new MacOsWorkflowRulesEditorMessage("user", message))
.ToArray();
Volatile.Write(ref snapshot, new MacOsWorkflowRulesEditorChatResponse(
Array.AsReadOnly(messages), true));
try
{
// Dispatch the agent away from the caller so opening a window or processing a decision stays responsive.
await Task.Run(() => viewModel.SendPromptAsync(message, applicationStopping), applicationStopping);
}
finally
{
var completedMessages = viewModel.Messages
.Where(item => item.Kind == WorkflowRulesEditorConversationItemKind.Message)
.Select(item => new MacOsWorkflowRulesEditorMessage(
item.Message!.Role.ToString().ToLowerInvariant(),
item.Message.Content))
.ToArray();
Volatile.Write(ref snapshot, new MacOsWorkflowRulesEditorChatResponse(
Array.AsReadOnly(completedMessages), false));
}
return GetSnapshot();
}
finally
{
sendLock.Release();
}
}
private MacOsWorkflowRulesEditorChatResponse Snapshot()
{
return new MacOsWorkflowRulesEditorChatResponse(
viewModel.Messages
.Where(item => item.Kind == WorkflowRulesEditorConversationItemKind.Message)
.Select(item => new MacOsWorkflowRulesEditorMessage(
item.Message!.Role.ToString().ToLowerInvariant(),
item.Message.Content))
.ToArray());
}
}
public sealed record MacOsWorkflowRulesEditorChatRequest(string? Message);
public sealed record MacOsWorkflowRulesEditorChatResponse(
IReadOnlyList<MacOsWorkflowRulesEditorMessage> Messages);
IReadOnlyList<MacOsWorkflowRulesEditorMessage> Messages,
bool IsThinking = false);
public sealed record MacOsWorkflowRulesEditorMessage(string Role, string Content);
@@ -126,6 +153,13 @@ public static class MacOsWorkflowRulesEditorPage
const prompt = document.querySelector('#prompt');
const send = document.querySelector('#send');
const messages = document.querySelector('#messages');
const session = new URLSearchParams(location.search).get('session');
const chatUrl = '/diagnostics/settings-and-logs/chat' + (session ? '?session=' + encodeURIComponent(session) : '');
let submitting = false;
let thinking = false;
let refreshing = false;
let available = true;
let refreshTimer;
const render = items => {
messages.replaceChildren(...items.map(item => {
const card = document.createElement('div');
@@ -135,22 +169,57 @@ public static class MacOsWorkflowRulesEditorPage
}));
messages.scrollTop = messages.scrollHeight;
};
const update = body => {
render(body.messages);
thinking = !!body.isThinking;
send.disabled = submitting || thinking;
};
const showError = text => {
const card = document.createElement('div');
card.className = 'message agent';
card.textContent = text;
messages.append(card);
};
const refresh = async () => {
if (refreshing || !available) return;
clearTimeout(refreshTimer);
refreshing = true;
try {
const response = await fetch(chatUrl, {cache: 'no-store'});
if (!response.ok) throw new Error('This conversation is no longer available.');
update(await response.json());
} catch (error) {
available = false;
send.disabled = true;
showError(error.message);
} finally {
refreshing = false;
if (available && (thinking || submitting)) refreshTimer = setTimeout(refresh, 300);
}
};
form.addEventListener('submit', async event => {
event.preventDefault();
const message = prompt.value.trim();
if (!message) return;
if (!message || submitting || thinking || !available) return;
prompt.value = '';
submitting = true;
send.disabled = true;
try {
const response = await fetch('/diagnostics/settings-and-logs/chat', {
const responseTask = fetch(chatUrl, {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({message})
});
const body = await response.json();
render(body.messages);
void refresh();
const response = await responseTask;
if (!response.ok) throw new Error('The message could not be sent.');
update(await response.json());
} catch (error) {
showError(error.message);
} finally {
send.disabled = false;
submitting = false;
send.disabled = !available || thinking;
void refresh();
prompt.focus();
}
});
@@ -160,6 +229,7 @@ public static class MacOsWorkflowRulesEditorPage
form.requestSubmit();
}
});
void refresh();
</script>
</body>
</html>