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
6 changes: 5 additions & 1 deletion src/OpenClaw.Tray.WinUI/Chat/FunctionalChatHostExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.UI.Xaml.Controls;
using OpenClawTray.FunctionalUI.Hosting;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace OpenClawTray.Chat;
Expand Down Expand Up @@ -81,7 +82,10 @@ public sealed class MountedFunctionalChat(Border target, FunctionalHostControl h
public OpenClawChatRoot ChatRoot => root;

/// <summary>Push a picked file into the composer as a pending attachment.</summary>
public void AttachFile(ChatAttachment attachment) => root.OnFileAttached?.Invoke(attachment);
public void AttachFile(ChatAttachment attachment) => AttachFiles(new[] { attachment });

/// <summary>Push picked files into the composer as pending attachments.</summary>
public void AttachFiles(IReadOnlyList<ChatAttachment> attachments) => root.OnFilesAttached?.Invoke(attachments);

/// <summary>Push streaming voice transcript text into the composer.</summary>
public void SetVoiceTranscript(string? text) => root.SetVoiceTranscript?.Invoke(text);
Expand Down
75 changes: 53 additions & 22 deletions src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public sealed class OpenClawChatRoot : Component
private readonly Action<bool>? _onSpeakerMuteChanged;
private readonly bool _initialMuted;
private readonly bool _isCompact;
private Action<ChatAttachment>? _onFileAttached;
private Action<IReadOnlyList<ChatAttachment>>? _onFilesAttached;
private Action<string?>? _setVoiceTranscript;
private Action<float>? _setVoiceAudioLevel;
private Action? _scrollToBottomToken;
Expand All @@ -56,12 +56,12 @@ public sealed class OpenClawChatRoot : Component

/// <summary>
/// Callback invoked by the host window/page after a file is selected.
/// Sets the pending attachment and triggers a re-render.
/// Appends pending attachments and triggers a re-render.
/// </summary>
public Action<ChatAttachment>? OnFileAttached
public Action<IReadOnlyList<ChatAttachment>>? OnFilesAttached
{
get => _onFileAttached;
set => _onFileAttached = value;
get => _onFilesAttached;
set => _onFilesAttached = value;
}

/// <summary>
Expand Down Expand Up @@ -109,7 +109,9 @@ public OpenClawChatRoot(

public override Element Render()
{
var pendingAttachment = UseState<ChatAttachment?>(null, threadSafe: true);
var pendingAttachments = UseState<IReadOnlyList<ChatAttachment>>(Array.Empty<ChatAttachment>(), threadSafe: true);
var pendingAttachmentsRef = UseRef<IReadOnlyList<ChatAttachment>>(pendingAttachments.Value);
pendingAttachmentsRef.Current = pendingAttachments.Value;
var speakerMuted = UseState(_initialMuted, threadSafe: true);
var voiceTranscript = UseState<string?>(null, threadSafe: true);
var voiceAudioLevel = UseState(0f, threadSafe: true);
Expand All @@ -122,9 +124,21 @@ public override Element Render()
// Cleared automatically when the next snapshot arrives.
var firstSendInFlight = UseState(false, threadSafe: true);

// Wire the OnFileAttached callback so the host window/page can set the
// pending attachment after the file picker completes.
_onFileAttached = att => pendingAttachment.Set(att);
void SetPendingAttachments(IReadOnlyList<ChatAttachment> attachments)
{
pendingAttachmentsRef.Current = attachments;
pendingAttachments.Set(attachments);
}

// Wire the attachment callback so the host window/page can append
// pending attachments after the file picker completes.
_onFilesAttached = attachments =>
{
if (attachments.Count == 0)
return;

SetPendingAttachments(pendingAttachmentsRef.Current.Concat(attachments).ToArray());
};
_setVoiceTranscript = voiceTranscript.Set;
_setVoiceAudioLevel = voiceAudioLevel.Set;
_scrollToBottomToken = () => scrollToBottomToken.Set(scrollToBottomToken.Value + 1);
Expand Down Expand Up @@ -452,7 +466,7 @@ Element BuildLoadingElement()
if (effectiveThread is { } t)
{
firstSendInFlight.Set(true);
OnSend(t.Id, suggestion, null);
OnSend(t.Id, suggestion, Array.Empty<ChatAttachment>());
}
}, suggestionsDisabled: firstSendInFlight.Value);
}
Expand Down Expand Up @@ -535,10 +549,10 @@ Element BuildLoadingElement()
AvailableModels: snapshot.AvailableModels,
CurrentModel: composerThread.Model,
CurrentThinkingLevel: composerThread.ThinkingLevel,
OnSend: (msg, att) =>
OnSend: (msg, attachments) =>
{
pendingAttachment.Set(null);
OnSend(composerThread.Id!, msg, att);
SetPendingAttachments(Array.Empty<ChatAttachment>());
OnSend(composerThread.Id!, msg, attachments);
},
OnStop: () => OnStop(composerThread.Id!),
OnChannelChanged: id =>
Expand All @@ -551,8 +565,8 @@ Element BuildLoadingElement()
OnPermissionsChanged: allowAll => RunFireAndForget(ct => _provider.SetPermissionModeAsync(composerThread.Id!, allowAll, ct)),
OnVoiceRequest: _onVoiceRequest,
OnAttachClick: _onAttachClick,
PendingAttachment: pendingAttachment.Value,
OnAttachmentRemoved: () => pendingAttachment.Set(null),
PendingAttachments: pendingAttachments.Value,
OnAttachmentRemoved: attachment => SetPendingAttachments(RemoveAttachment(pendingAttachmentsRef.Current, attachment)),
IsSpeakerMuted: speakerMuted.Value,
OnSpeakerToggle: () =>
{
Expand All @@ -564,7 +578,7 @@ Element BuildLoadingElement()
VoiceTranscript: voiceTranscript.Value,
VoiceAudioLevel: voiceAudioLevel.Value,
RegisterVoiceStarter: starter => TriggerVoiceRecording = starter,
OnAttachmentPasted: att => pendingAttachment.Set(att),
OnAttachmentPasted: att => SetPendingAttachments(pendingAttachmentsRef.Current.Concat(new[] { att }).ToArray()),
ShowToolCalls: showToolCalls.Value,
OnShowToolCallsChanged: visible =>
{
Expand Down Expand Up @@ -837,18 +851,35 @@ private static Element PlaceholderEmptyThreadState(string connectionState)
);
}

private void OnSend(string threadId, string message, ChatAttachment? attachment)
private void OnSend(string threadId, string message, IReadOnlyList<ChatAttachment> attachments)
{
_scrollToBottomToken?.Invoke();
IReadOnlyList<ChatAttachment>? attachments = attachment is not null
? new[] { attachment }
: null;
if (attachments is not null)
RunFireAndForget(ct => _provider.SendMessageAsync(threadId, message, ct, attachments));
if (attachments.Count > 0)
RunFireAndForget(ct => _provider.SendMessageAsync(threadId, message, ct, attachments.ToArray()));
else
RunFireAndForget(ct => _provider.SendMessageAsync(threadId, message, ct));
}

private static IReadOnlyList<ChatAttachment> RemoveAttachment(
IReadOnlyList<ChatAttachment> attachments,
ChatAttachment attachment)
{
var next = new List<ChatAttachment>(attachments.Count);
var removed = false;
foreach (var current in attachments)
{
if (!removed && ReferenceEquals(current, attachment))
{
removed = true;
continue;
}

next.Add(current);
}

return removed ? next.ToArray() : attachments;
}

private void OnStop(string threadId)
{
RunFireAndForget(ct => _provider.StopResponseAsync(threadId, ct));
Expand Down
Loading