Skip to content

Commit ef25dbf

Browse files
committed
improve compilot initialization
1 parent 9943a46 commit ef25dbf

3 files changed

Lines changed: 161 additions & 15 deletions

File tree

src/OneWare.Chat/ViewModels/ChatViewModel.cs

Lines changed: 148 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ public partial class ChatViewModel : ExtendedTool, IChatManagerService
3434
private readonly Dictionary<string, List<ChatSessionHistoryItem>> _historyByService = new(StringComparer.Ordinal);
3535

3636
private bool _initialized;
37+
38+
// Single-flight guard: initialization is kicked off automatically when a service is
39+
// selected. A send that happens while that is still running must join the running
40+
// operation instead of starting a second (destructive) initialization.
41+
private Task<bool>? _initializeTask;
42+
private IChatService? _initializeTaskService;
43+
44+
// Completion sources waiting for the selected service to report a connected status.
45+
private readonly List<TaskCompletionSource<bool>> _connectionWaiters = [];
46+
47+
private static readonly TimeSpan ConnectionWaitTimeout = TimeSpan.FromMinutes(2);
3748
// FIFO of messages sent locally so the echoed ChatUserMessageEvent can be matched
3849
// (suppressed for normal/steered sends, or used to activate a queued message).
3950
private readonly Queue<PendingLocalMessage> _pendingLocalMessages = new();
@@ -136,6 +147,41 @@ public bool IsConnected
136147
SteerCommand.NotifyCanExecuteChanged();
137148
QueueCommand.NotifyCanExecuteChanged();
138149
AbortCommand.NotifyCanExecuteChanged();
150+
151+
if (value) ReleaseConnectionWaiters(true);
152+
}
153+
}
154+
}
155+
156+
/// <summary>
157+
/// True while a send is waiting for the selected service to finish connecting.
158+
/// </summary>
159+
public bool IsWaitingForConnection
160+
{
161+
get;
162+
private set
163+
{
164+
if (SetProperty(ref field, value))
165+
{
166+
SendCommand.NotifyCanExecuteChanged();
167+
SteerCommand.NotifyCanExecuteChanged();
168+
QueueCommand.NotifyCanExecuteChanged();
169+
}
170+
}
171+
}
172+
173+
/// <summary>
174+
/// True while the selected service is still initializing/starting up. Sending is allowed in
175+
/// this state; the message is held back until the connection is up.
176+
/// </summary>
177+
public bool IsConnecting
178+
{
179+
get;
180+
private set
181+
{
182+
if (SetProperty(ref field, value))
183+
{
184+
SendCommand.NotifyCanExecuteChanged();
139185
}
140186
}
141187
}
@@ -194,6 +240,7 @@ public IChatService? SelectedChatService
194240
oldValue.EventReceived -= OnEventReceived;
195241
oldValue.StatusChanged -= OnStatusChanged;
196242
oldValue.SessionReset -= OnSessionReset;
243+
ReleaseConnectionWaiters(false);
197244
}
198245

199246
if (value == null) return;
@@ -234,15 +281,88 @@ public override void InitializeContent()
234281

235282
private async Task<bool> InitializeCurrentAsync()
236283
{
237-
if (SelectedChatService == null) return false;
238-
239-
var status = await SelectedChatService.InitializeAsync();
284+
var service = SelectedChatService;
285+
if (service == null) return false;
286+
287+
// Join an initialization that is already running for this service instead of
288+
// starting a second one (a concurrent initialization tears down the client the
289+
// first one is still setting up).
290+
if (_initializeTask is { } running && _initializeTaskService == service)
291+
{
292+
return await running;
293+
}
294+
295+
var task = InitializeServiceAsync(service);
296+
_initializeTask = task;
297+
_initializeTaskService = service;
298+
IsConnecting = true;
299+
300+
try
301+
{
302+
return await task;
303+
}
304+
finally
305+
{
306+
if (ReferenceEquals(_initializeTask, task))
307+
{
308+
_initializeTask = null;
309+
_initializeTaskService = null;
310+
IsConnecting = false;
311+
}
312+
}
313+
}
314+
315+
private async Task<bool> InitializeServiceAsync(IChatService service)
316+
{
317+
var status = await service.InitializeAsync();
318+
319+
if (SelectedChatService != service) return status;
240320

241321
IsInitialized = status;
242322

323+
if (!status) ReleaseConnectionWaiters(false);
324+
243325
return status;
244326
}
245327

328+
/// <summary>
329+
/// Waits until the selected service reports a connected status, instead of failing a send
330+
/// that arrives while the service is still starting up.
331+
/// </summary>
332+
private async Task<bool> WaitForConnectionAsync()
333+
{
334+
if (IsConnected) return true;
335+
336+
var waiter = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
337+
_connectionWaiters.Add(waiter);
338+
339+
IsWaitingForConnection = true;
340+
341+
try
342+
{
343+
return await waiter.Task.WaitAsync(ConnectionWaitTimeout);
344+
}
345+
catch (TimeoutException)
346+
{
347+
return false;
348+
}
349+
finally
350+
{
351+
_connectionWaiters.Remove(waiter);
352+
IsWaitingForConnection = _connectionWaiters.Count > 0;
353+
}
354+
}
355+
356+
private void ReleaseConnectionWaiters(bool connected)
357+
{
358+
if (_connectionWaiters.Count == 0) return;
359+
360+
foreach (var waiter in _connectionWaiters.ToArray())
361+
{
362+
waiter.TrySetResult(connected);
363+
}
364+
}
365+
246366
private async Task InitializeAndRestoreCurrentServiceAsync(IChatService chatService)
247367
{
248368
LoadMessagesForService(chatService.Name);
@@ -293,27 +413,40 @@ private async Task SendInternalAsync(ChatSendMode mode)
293413
var prompt = CurrentMessage.Trim();
294414
if (string.IsNullOrWhiteSpace(prompt)) return;
295415

296-
if (SelectedChatService == null)
416+
var chatService = SelectedChatService;
417+
if (chatService == null)
297418
{
298419
AddErrorMessage("No chat service selected.");
299420
return;
300421
}
301422

302-
if (!IsInitialized)
423+
var initialized = IsInitialized;
424+
if (!initialized)
303425
{
304-
await InitializeCurrentAsync();
426+
initialized = await InitializeCurrentAsync();
305427
}
306428

307429
if (!IsConnected)
308430
{
309-
// Replace (don't stack) the transient connection warning; it is removed
310-
// again as soon as a message actually goes through.
311-
if (_notConnectedMessage != null) Messages.Remove(_notConnectedMessage);
312-
_notConnectedMessage = new ChatMessageErrorViewModel($"{SelectedChatService.Name} is not connected yet.");
313-
AddMessage(_notConnectedMessage);
314-
return;
431+
// The service may still be starting up (typical right after IDE launch).
432+
// Wait for it to connect instead of rejecting the message.
433+
var connected = initialized && await WaitForConnectionAsync();
434+
435+
if (!connected)
436+
{
437+
// Replace (don't stack) the transient connection warning; it is removed
438+
// again as soon as a message actually goes through.
439+
if (_notConnectedMessage != null) Messages.Remove(_notConnectedMessage);
440+
_notConnectedMessage =
441+
new ChatMessageErrorViewModel($"{chatService.Name} is not connected yet.");
442+
AddMessage(_notConnectedMessage);
443+
return;
444+
}
315445
}
316446

447+
// The user may have switched services while we were waiting for the connection.
448+
if (!ReferenceEquals(SelectedChatService, chatService)) return;
449+
317450
if (_notConnectedMessage != null)
318451
{
319452
Messages.Remove(_notConnectedMessage);
@@ -354,7 +487,7 @@ private async Task SendInternalAsync(ChatSendMode mode)
354487

355488
try
356489
{
357-
await SelectedChatService.SendAsync(prompt, mode);
490+
await chatService.SendAsync(prompt, mode);
358491
}
359492
catch (Exception ex)
360493
{
@@ -435,7 +568,8 @@ private async Task RemoveQueuedMessageAsync(ChatMessageUserViewModel? message)
435568
private bool CanRemoveQueuedMessage(ChatMessageUserViewModel? message) =>
436569
message != null && ReferenceEquals(QueuedMessages.LastOrDefault(), message);
437570

438-
private bool CanSend() => IsConnected && !string.IsNullOrWhiteSpace(CurrentMessage);
571+
private bool CanSend() => (IsConnected || IsConnecting) && !IsWaitingForConnection &&
572+
!string.IsNullOrWhiteSpace(CurrentMessage);
439573

440574
private bool CanAbort() => IsConnected && IsBusy;
441575

src/OneWare.Chat/Views/ChatView.axaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@
9595
Text="{Binding WorkingStatusText}" />
9696
</Grid>
9797

98+
<!-- Waiting for the chat service to finish connecting -->
99+
<Grid ColumnDefinitions="Auto, Auto" ColumnSpacing="5"
100+
IsVisible="{Binding IsWaitingForConnection}">
101+
<controls:Spinner Width="12" Height="12" VerticalAlignment="Center" />
102+
103+
<TextBlock Grid.Column="1"
104+
Foreground="{DynamicResource ThemeForegroundLowBrush}" FontSize="11"
105+
FontStyle="Italic"
106+
Text="{Binding StatusText}" />
107+
</Grid>
108+
98109
<!-- Queued messages (dimmed, pinned below the conversation) -->
99110
<ItemsControl Opacity="0.5" ItemsSource="{Binding QueuedMessages}">
100111
<ItemsControl.ItemTemplate>

src/OneWare.Copilot/Services/CopilotChatService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -864,10 +864,11 @@ public async Task<bool> InitializeAsync()
864864

865865

866866
await _sync.WaitAsync().ConfigureAwait(false);
867-
await DisposeAsync();
868867

869868
try
870869
{
870+
await DisposeAsync();
871+
871872
if (!PlatformHelper.ExistsOnPath(cliPath))
872873
{
873874
StatusChanged?.Invoke(this, new StatusEvent(false, "CLI Not found"));

0 commit comments

Comments
 (0)