@memberjunction/ng-conversations 2.105.0 → 2.107.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts +19 -13
  2. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
  3. package/dist/lib/components/conversation/conversation-chat-area.component.js +120 -122
  4. package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
  5. package/dist/lib/components/message/message-input.component.d.ts +64 -0
  6. package/dist/lib/components/message/message-input.component.d.ts.map +1 -1
  7. package/dist/lib/components/message/message-input.component.js +251 -107
  8. package/dist/lib/components/message/message-input.component.js.map +1 -1
  9. package/dist/lib/components/message/message-list.component.d.ts +3 -5
  10. package/dist/lib/components/message/message-list.component.d.ts.map +1 -1
  11. package/dist/lib/components/message/message-list.component.js +38 -9
  12. package/dist/lib/components/message/message-list.component.js.map +1 -1
  13. package/dist/lib/models/lazy-artifact-info.d.ts +68 -0
  14. package/dist/lib/models/lazy-artifact-info.d.ts.map +1 -0
  15. package/dist/lib/models/lazy-artifact-info.js +150 -0
  16. package/dist/lib/models/lazy-artifact-info.js.map +1 -0
  17. package/dist/lib/services/conversation-agent.service.d.ts +11 -0
  18. package/dist/lib/services/conversation-agent.service.d.ts.map +1 -1
  19. package/dist/lib/services/conversation-agent.service.js +138 -5
  20. package/dist/lib/services/conversation-agent.service.js.map +1 -1
  21. package/dist/public-api.d.ts +1 -0
  22. package/dist/public-api.d.ts.map +1 -1
  23. package/dist/public-api.js +1 -0
  24. package/dist/public-api.js.map +1 -1
  25. package/package.json +12 -12
@@ -19,8 +19,12 @@ function MessageInputComponent_div_6_Template(rf, ctx) { if (rf & 1) {
19
19
  i0.ɵɵelementStart(0, "div", 10);
20
20
  i0.ɵɵelement(1, "i", 11);
21
21
  i0.ɵɵelementStart(2, "span");
22
- i0.ɵɵtext(3, "AI is responding...");
22
+ i0.ɵɵtext(3);
23
23
  i0.ɵɵelementEnd()();
24
+ } if (rf & 2) {
25
+ const ctx_r1 = i0.ɵɵnextContext();
26
+ i0.ɵɵadvance(3);
27
+ i0.ɵɵtextInterpolate(ctx_r1.processingMessage);
24
28
  } }
25
29
  export class MessageInputComponent {
26
30
  dialogService;
@@ -48,6 +52,7 @@ export class MessageInputComponent {
48
52
  messageText = '';
49
53
  isSending = false;
50
54
  isProcessing = false; // True when waiting for agent/naming response
55
+ processingMessage = 'AI is responding...'; // Message shown during processing
51
56
  converationManagerAgent = null;
52
57
  // Mention autocomplete state
53
58
  showMentionDropdown = false;
@@ -60,6 +65,8 @@ export class MessageInputComponent {
60
65
  pushStatusSubscription;
61
66
  // Track active task execution message IDs for real-time updates
62
67
  activeTaskExecutionMessageIds = new Set();
68
+ // Track completion timestamps to prevent race conditions with late progress updates
69
+ completionTimestamps = new Map();
63
70
  constructor(dialogService, toastService, agentService, conversationState, dataCache, activeTasks, mentionAutocomplete, mentionParser) {
64
71
  this.dialogService = dialogService;
65
72
  this.toastService = toastService;
@@ -298,94 +305,174 @@ export class MessageInputComponent {
298
305
  return;
299
306
  this.isSending = true;
300
307
  try {
301
- const detail = await this.dataCache.createConversationDetail(this.currentUser);
302
- detail.ConversationID = this.conversationId;
303
- detail.Message = this.messageText.trim();
304
- detail.Role = 'User';
305
- // Parse mentions from message (not stored, used for routing only)
306
- const mentionResult = this.mentionParser.parseMentions(detail.Message, this.mentionAutocomplete.getAvailableAgents(), this.mentionAutocomplete.getAvailableUsers());
307
- console.log('[MentionInput] Parsing message for routing:', detail.Message);
308
- console.log('[MentionInput] Found mentions:', mentionResult);
309
- console.log('[MentionInput] Agent mention:', mentionResult.agentMention);
310
- // Set ParentID if this is a thread reply
311
- if (this.parentMessageId) {
312
- detail.ParentID = this.parentMessageId;
313
- }
314
- const saved = await detail.Save();
308
+ const messageDetail = await this.createMessageDetail();
309
+ const saved = await messageDetail.Save();
315
310
  if (saved) {
316
- this.messageSent.emit(detail);
317
- this.messageText = '';
318
- // Check if this is the first message in the conversation
319
- const isFirstMessage = this.conversationHistory.length === 0;
320
- // Determine routing: @mention > last agent context > Sage
321
- if (mentionResult.agentMention) {
322
- // Direct @mention - skip Sage, invoke agent directly
323
- console.log('🎯 Direct @mention detected, bypassing Sage');
324
- if (isFirstMessage) {
325
- Promise.all([
326
- this.invokeAgentDirectly(detail, mentionResult.agentMention, this.conversationId),
327
- this.nameConversation(detail.Message)
328
- ]);
329
- }
330
- else {
331
- this.invokeAgentDirectly(detail, mentionResult.agentMention, this.conversationId);
332
- }
333
- }
334
- else {
335
- // Check if user is replying to an agent (implicit continuation)
336
- const lastAIMessage = this.conversationHistory
337
- .slice()
338
- .reverse()
339
- .find(msg => msg.Role === 'AI' &&
340
- msg.AgentID &&
341
- msg.AgentID !== this.converationManagerAgent?.ID);
342
- if (lastAIMessage && lastAIMessage.AgentID) {
343
- // Continue with same agent - skip Sage
344
- console.log('🔄 Implicit continuation detected, continuing with last agent');
345
- if (isFirstMessage) {
346
- Promise.all([
347
- this.continueWithAgent(detail, lastAIMessage.AgentID, this.conversationId),
348
- this.nameConversation(detail.Message)
349
- ]);
350
- }
351
- else {
352
- this.continueWithAgent(detail, lastAIMessage.AgentID, this.conversationId);
353
- }
354
- }
355
- else {
356
- // No context - use Sage
357
- console.log('🤖 No agent context, using Sage');
358
- if (isFirstMessage) {
359
- Promise.all([
360
- this.processMessageThroughAgent(detail, mentionResult),
361
- this.nameConversation(detail.Message)
362
- ]);
363
- }
364
- else {
365
- this.processMessageThroughAgent(detail, mentionResult);
366
- }
367
- }
368
- }
369
- // Focus back on textarea
370
- setTimeout(() => {
371
- if (this.messageTextarea && this.messageTextarea.nativeElement) {
372
- this.messageTextarea.nativeElement.focus();
373
- }
374
- }, 100);
311
+ await this.handleSuccessfulSend(messageDetail);
375
312
  }
376
313
  else {
377
- console.error('Failed to send message:', detail.LatestResult?.Message);
378
- this.toastService.error('Failed to send message. Please try again.');
314
+ this.handleSendFailure(messageDetail);
379
315
  }
380
316
  }
381
317
  catch (error) {
382
- console.error('Error sending message:', error);
383
- this.toastService.error('Error sending message. Please try again.');
318
+ this.handleSendError(error);
384
319
  }
385
320
  finally {
386
321
  this.isSending = false;
387
322
  }
388
323
  }
324
+ /**
325
+ * Creates and configures a new conversation detail message
326
+ */
327
+ async createMessageDetail() {
328
+ const detail = await this.dataCache.createConversationDetail(this.currentUser);
329
+ detail.ConversationID = this.conversationId;
330
+ detail.Message = this.messageText.trim();
331
+ detail.Role = 'User';
332
+ if (this.parentMessageId) {
333
+ detail.ParentID = this.parentMessageId;
334
+ }
335
+ return detail;
336
+ }
337
+ /**
338
+ * Handles successful message send - routes to appropriate agent
339
+ */
340
+ async handleSuccessfulSend(messageDetail) {
341
+ this.messageSent.emit(messageDetail);
342
+ this.messageText = '';
343
+ const mentionResult = this.parseMentionsFromMessage(messageDetail.Message);
344
+ const isFirstMessage = this.conversationHistory.length === 0;
345
+ await this.routeMessage(messageDetail, mentionResult, isFirstMessage);
346
+ this.refocusTextarea();
347
+ }
348
+ /**
349
+ * Parses mentions from the message for routing decisions
350
+ */
351
+ parseMentionsFromMessage(message) {
352
+ const mentionResult = this.mentionParser.parseMentions(message, this.mentionAutocomplete.getAvailableAgents(), this.mentionAutocomplete.getAvailableUsers());
353
+ console.log('[MentionInput] Parsing message for routing:', message);
354
+ console.log('[MentionInput] Found mentions:', mentionResult);
355
+ console.log('[MentionInput] Agent mention:', mentionResult.agentMention);
356
+ return mentionResult;
357
+ }
358
+ /**
359
+ * Routes the message to the appropriate agent or Sage based on context
360
+ * Priority: @mention > intent check > Sage
361
+ */
362
+ async routeMessage(messageDetail, mentionResult, isFirstMessage) {
363
+ // Priority 1: Direct @mention
364
+ if (mentionResult.agentMention) {
365
+ await this.handleDirectMention(messageDetail, mentionResult.agentMention, isFirstMessage);
366
+ return;
367
+ }
368
+ // Priority 2: Check for previous agent with intent check
369
+ const lastAgentId = this.findLastNonSageAgentId();
370
+ if (lastAgentId) {
371
+ await this.handleAgentContinuity(messageDetail, lastAgentId, mentionResult, isFirstMessage);
372
+ return;
373
+ }
374
+ // Priority 3: No context - use Sage
375
+ await this.handleNoAgentContext(messageDetail, mentionResult, isFirstMessage);
376
+ }
377
+ /**
378
+ * Handles routing when user directly mentions an agent with @
379
+ */
380
+ async handleDirectMention(messageDetail, agentMention, isFirstMessage) {
381
+ console.log('🎯 Direct @mention detected, bypassing Sage');
382
+ await this.executeRouteWithNaming(() => this.invokeAgentDirectly(messageDetail, agentMention, this.conversationId), messageDetail.Message, isFirstMessage);
383
+ }
384
+ /**
385
+ * Handles routing when there's a previous agent - checks intent first
386
+ */
387
+ async handleAgentContinuity(messageDetail, lastAgentId, mentionResult, isFirstMessage) {
388
+ console.log('🔍 Previous agent found, checking continuity intent...');
389
+ const intent = await this.checkContinuityIntent(lastAgentId, messageDetail.Message);
390
+ if (intent === 'YES') {
391
+ console.log('✅ Intent check: YES - continuing with previous agent');
392
+ await this.executeRouteWithNaming(() => this.continueWithAgent(messageDetail, lastAgentId, this.conversationId), messageDetail.Message, isFirstMessage);
393
+ }
394
+ else {
395
+ console.log(`🤖 Intent check: ${intent} - routing through Sage for evaluation`);
396
+ await this.executeRouteWithNaming(() => this.processMessageThroughAgent(messageDetail, mentionResult), messageDetail.Message, isFirstMessage);
397
+ }
398
+ }
399
+ /**
400
+ * Handles routing when there's no previous agent context
401
+ */
402
+ async handleNoAgentContext(messageDetail, mentionResult, isFirstMessage) {
403
+ console.log('🤖 No agent context, using Sage');
404
+ await this.executeRouteWithNaming(() => this.processMessageThroughAgent(messageDetail, mentionResult), messageDetail.Message, isFirstMessage);
405
+ }
406
+ /**
407
+ * Finds the last agent ID that isn't Sage
408
+ */
409
+ findLastNonSageAgentId() {
410
+ const lastAIMessage = this.conversationHistory
411
+ .slice()
412
+ .reverse()
413
+ .find(msg => msg.Role === 'AI' &&
414
+ msg.AgentID &&
415
+ msg.AgentID !== this.converationManagerAgent?.ID);
416
+ return lastAIMessage?.AgentID || null;
417
+ }
418
+ /**
419
+ * Checks if message should continue with the previous agent
420
+ * Shows UI indicator during check
421
+ */
422
+ async checkContinuityIntent(agentId, message) {
423
+ this.processingMessage = 'Analyzing intent...';
424
+ this.isProcessing = true;
425
+ try {
426
+ const intent = await this.agentService.checkAgentContinuityIntent(agentId, message, this.conversationHistory);
427
+ return intent;
428
+ }
429
+ catch (error) {
430
+ console.error('❌ Intent check failed, defaulting to UNSURE:', error);
431
+ return 'UNSURE';
432
+ }
433
+ finally {
434
+ this.processingMessage = 'AI is responding...';
435
+ this.isProcessing = false;
436
+ }
437
+ }
438
+ /**
439
+ * Executes a routing function, optionally with conversation naming for first message
440
+ */
441
+ async executeRouteWithNaming(routeFunction, userMessage, isFirstMessage) {
442
+ if (isFirstMessage) {
443
+ await Promise.all([
444
+ routeFunction(),
445
+ this.nameConversation(userMessage)
446
+ ]);
447
+ }
448
+ else {
449
+ await routeFunction();
450
+ }
451
+ }
452
+ /**
453
+ * Returns focus to the message textarea
454
+ */
455
+ refocusTextarea() {
456
+ setTimeout(() => {
457
+ if (this.messageTextarea?.nativeElement) {
458
+ this.messageTextarea.nativeElement.focus();
459
+ }
460
+ }, 100);
461
+ }
462
+ /**
463
+ * Handles message send failure
464
+ */
465
+ handleSendFailure(messageDetail) {
466
+ console.error('Failed to send message:', messageDetail.LatestResult?.Message);
467
+ this.toastService.error('Failed to send message. Please try again.');
468
+ }
469
+ /**
470
+ * Handles message send error
471
+ */
472
+ handleSendError(error) {
473
+ console.error('Error sending message:', error);
474
+ this.toastService.error('Error sending message. Please try again.');
475
+ }
389
476
  /**
390
477
  * Safe save for ConversationDetail - prevents overwrites of completed/errored messages
391
478
  * Use this ONLY in progress update paths to prevent race conditions
@@ -408,9 +495,23 @@ export class MessageInputComponent {
408
495
  * IMPORTANT: Filters by agentRunId to prevent cross-contamination when multiple agents run in parallel
409
496
  */
410
497
  createProgressCallback(conversationDetail, agentName) {
498
+ // Use closure to capture the agent run ID from the first progress message
499
+ // This allows us to filter out progress messages from other concurrent agents
500
+ let capturedAgentRunId = null;
411
501
  return async (progress) => {
412
502
  // Extract agentRunId from progress metadata
413
503
  const progressAgentRunId = progress.metadata?.agentRunId;
504
+ // Capture the agent run ID from the first progress message
505
+ if (!capturedAgentRunId && progressAgentRunId) {
506
+ capturedAgentRunId = progressAgentRunId;
507
+ console.log(`[${agentName}] 📌 Captured agent run ID: ${capturedAgentRunId} for conversation detail: ${conversationDetail.ID}`);
508
+ }
509
+ // Filter out progress messages from other concurrent agents
510
+ // This prevents cross-contamination when multiple agents run in parallel
511
+ if (capturedAgentRunId && progressAgentRunId && progressAgentRunId !== capturedAgentRunId) {
512
+ console.log(`[${agentName}] 🚫 Ignoring progress from different agent run (expected: ${capturedAgentRunId}, got: ${progressAgentRunId})`);
513
+ return;
514
+ }
414
515
  // Format progress message with visual indicator
415
516
  const progressText = progress.message;
416
517
  // Update the active task with progress details (if it exists)
@@ -419,10 +520,16 @@ export class MessageInputComponent {
419
520
  try {
420
521
  if (conversationDetail) {
421
522
  console.log(`[${agentName}] Got conversation detail from cache - Status: ${conversationDetail.Status}, ID: ${conversationDetail.ID}`);
422
- // Skip progress updates if message is already complete
423
- // Since we're using the cached instance, this check sees the ACTUAL current state
424
- if (conversationDetail.Status === 'Complete') {
425
- console.log(`[${agentName}] ⛔ Skipping progress update - message already complete`);
523
+ // Check 1: Skip if message is already complete or errored
524
+ if (conversationDetail.Status === 'Complete' || conversationDetail.Status === 'Error') {
525
+ console.log(`[${agentName}] ⛔ Skipping progress update - message status is ${conversationDetail.Status}`);
526
+ return;
527
+ }
528
+ // Check 2: Skip if message was marked as completed (prevents race condition)
529
+ // Once a message is marked complete, we reject ALL further progress updates
530
+ const completionTime = this.completionTimestamps.get(conversationDetail.ID);
531
+ if (completionTime) {
532
+ console.log(`[${agentName}] ⛔ Skipping progress update - message was marked complete at ${completionTime}`);
426
533
  return;
427
534
  }
428
535
  // Emit agentRunId if we have it (for parent to track)
@@ -492,7 +599,8 @@ export class MessageInputComponent {
492
599
  taskId = null;
493
600
  }
494
601
  if (!result || !result.success) {
495
- // Evaluation failed
602
+ // Evaluation failed - mark as complete to stop progress updates
603
+ this.markMessageComplete(conversationManagerMessage);
496
604
  conversationManagerMessage.Status = 'Error';
497
605
  conversationManagerMessage.Message = `❌ Evaluation failed`;
498
606
  conversationManagerMessage.Error = result?.agentRun?.ErrorMessage || 'Agent evaluation failed';
@@ -502,6 +610,8 @@ export class MessageInputComponent {
502
610
  await userMessage.Save();
503
611
  this.messageSent.emit(userMessage);
504
612
  console.warn('⚠️ Sage failed:', result?.agentRun?.ErrorMessage);
613
+ // Clean up completion timestamp
614
+ this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
505
615
  return;
506
616
  }
507
617
  console.log('🤖 Sage Response:', {
@@ -531,6 +641,8 @@ export class MessageInputComponent {
531
641
  }
532
642
  // Stage 4: Direct chat response from Sage
533
643
  else if (result.agentRun.FinalStep === 'Chat' && result.agentRun.Message) {
644
+ // Mark message as completing BEFORE setting final content (prevents race condition)
645
+ this.markMessageComplete(conversationManagerMessage);
534
646
  // Normal chat response
535
647
  conversationManagerMessage.Message = result.agentRun.Message;
536
648
  conversationManagerMessage.Status = 'Complete';
@@ -549,26 +661,35 @@ export class MessageInputComponent {
549
661
  if (taskId) {
550
662
  this.activeTasks.remove(taskId);
551
663
  }
664
+ // Clean up completion timestamp after delay
665
+ this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
552
666
  }
553
667
  // Stage 5: Silent observation - but check for message content first
554
668
  else {
555
669
  // Check if there's a message to display even without payload/taskGraph
556
670
  if (result.agentRun.Message) {
557
671
  console.log('💬 Sage provided a message without payload');
558
- conversationManagerMessage.Message = result.agentRun.Message;
559
- conversationManagerMessage.Status = 'Complete';
672
+ // Mark message as completing BEFORE setting final content
673
+ this.markMessageComplete(conversationManagerMessage);
560
674
  conversationManagerMessage.HiddenToUser = false;
561
- await conversationManagerMessage.Save();
675
+ // use update helper to ensure that if there is a race condition with more streaming updates we don't allow that to override this final message
676
+ await this.updateConversationDetail(conversationManagerMessage, result.agentRun.Message, 'Complete');
562
677
  this.messageSent.emit(conversationManagerMessage);
678
+ // Clean up completion timestamp after delay
679
+ this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
563
680
  }
564
681
  else {
565
682
  console.log('🔇 Sage chose to observe silently');
683
+ // Mark message as completing
684
+ this.markMessageComplete(conversationManagerMessage);
566
685
  // Hide the Sage message
567
686
  conversationManagerMessage.HiddenToUser = true;
568
- conversationManagerMessage.Status = 'Complete';
569
- await conversationManagerMessage.Save();
687
+ // use update helper to ensure that if there is a race condition with more streaming updates we don't allow that to override this final message
688
+ await this.updateConversationDetail(conversationManagerMessage, conversationManagerMessage.Message, 'Complete');
570
689
  this.messageSent.emit(conversationManagerMessage);
571
690
  await this.handleSilentObservation(userMessage, this.conversationId);
691
+ // Clean up completion timestamp after delay
692
+ this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
572
693
  }
573
694
  // Remove CM from active tasks
574
695
  if (taskId) {
@@ -580,11 +701,15 @@ export class MessageInputComponent {
580
701
  console.error('❌ Error processing message through agents:', error);
581
702
  // Update conversationManagerMessage status to Error
582
703
  if (conversationManagerMessage && conversationManagerMessage.ID) {
704
+ // Mark as complete to stop progress updates
705
+ this.markMessageComplete(conversationManagerMessage);
583
706
  conversationManagerMessage.Status = 'Error';
584
707
  conversationManagerMessage.Message = `❌ Error: ${String(error)}`;
585
708
  conversationManagerMessage.Error = String(error);
586
709
  await conversationManagerMessage.Save();
587
710
  this.messageSent.emit(conversationManagerMessage);
711
+ // Clean up completion timestamp
712
+ this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
588
713
  }
589
714
  // Mark user message as complete
590
715
  userMessage.Status = 'Complete';
@@ -685,27 +810,20 @@ export class MessageInputComponent {
685
810
  };
686
811
  const result = await GraphQLDataProvider.Instance.ExecuteGQL(mutation, variables);
687
812
  console.log('📊 ExecuteTaskGraph result:', {
688
- hasData: !!result?.data,
689
- hasErrors: !!result?.errors,
690
- data: result?.data,
691
- errors: result?.errors
813
+ hasExecuteTaskGraph: !!result?.ExecuteTaskGraph,
814
+ success: result?.ExecuteTaskGraph?.success,
815
+ resultsCount: result?.ExecuteTaskGraph?.results?.length,
816
+ result: result
692
817
  });
693
818
  // Step 4: Update task execution message with results
694
- // Check for GraphQL errors first
695
- if (result?.errors && result.errors.length > 0) {
696
- const errorMsg = result.errors.map((e) => e.message).join(', ');
697
- console.error('❌ GraphQL errors:', result.errors);
698
- taskExecutionMessage.Message = `❌ **${workflowName}** failed: ${errorMsg}`;
699
- taskExecutionMessage.Status = 'Error';
700
- taskExecutionMessage.Error = errorMsg;
701
- }
702
- else if (result?.data?.ExecuteTaskGraph?.success) {
819
+ // ExecuteGQL returns data directly (not wrapped in {data, errors})
820
+ if (result?.ExecuteTaskGraph?.success) {
703
821
  console.log('✅ Task graph execution completed successfully');
704
822
  taskExecutionMessage.Message = `✅ **${workflowName}** completed successfully`;
705
823
  taskExecutionMessage.Status = 'Complete';
706
824
  }
707
825
  else {
708
- const errorMsg = result?.data?.ExecuteTaskGraph?.errorMessage || 'Unknown error';
826
+ const errorMsg = result?.ExecuteTaskGraph?.errorMessage || 'Unknown error';
709
827
  console.error('❌ Task graph execution failed:', errorMsg);
710
828
  taskExecutionMessage.Message = `❌ **${workflowName}** failed: ${errorMsg}`;
711
829
  taskExecutionMessage.Status = 'Error';
@@ -738,6 +856,10 @@ export class MessageInputComponent {
738
856
  if (convoDetail.Status === 'Complete' || convoDetail.Status === 'Error') {
739
857
  return; // Do not update completed or errored messages
740
858
  }
859
+ // Mark as completing BEFORE updating if status is Complete or Error
860
+ if (status === 'Complete' || status === 'Error') {
861
+ this.markMessageComplete(convoDetail);
862
+ }
741
863
  const maxAttempts = 2;
742
864
  let attempts = 0, done = false;
743
865
  while (attempts < maxAttempts && !done) {
@@ -753,6 +875,10 @@ export class MessageInputComponent {
753
875
  }
754
876
  attempts++;
755
877
  }
878
+ // Clean up completion timestamp after delay
879
+ if (status === 'Complete' || status === 'Error') {
880
+ this.cleanupCompletionTimestamp(convoDetail.ID);
881
+ }
756
882
  }
757
883
  /**
758
884
  * Handle single task execution from task graph using direct agent execution
@@ -1445,13 +1571,30 @@ export class MessageInputComponent {
1445
1571
  // Don't show error to user - naming failures should be silent
1446
1572
  }
1447
1573
  }
1574
+ /**
1575
+ * Marks a conversation detail as complete and records timestamp to prevent race conditions
1576
+ */
1577
+ markMessageComplete(conversationDetail) {
1578
+ const now = Date.now();
1579
+ this.completionTimestamps.set(conversationDetail.ID, now);
1580
+ console.log(`🏁 Marked message ${conversationDetail.ID} as complete at ${now}`);
1581
+ }
1582
+ /**
1583
+ * Cleans up completion timestamps for completed messages (prevents memory leak)
1584
+ */
1585
+ cleanupCompletionTimestamp(conversationDetailId) {
1586
+ // Keep timestamp for a short period to catch any late progress updates
1587
+ setTimeout(() => {
1588
+ this.completionTimestamps.delete(conversationDetailId);
1589
+ }, 5000); // 5 seconds should be more than enough
1590
+ }
1448
1591
  static ɵfac = function MessageInputComponent_Factory(t) { return new (t || MessageInputComponent)(i0.ɵɵdirectiveInject(i1.DialogService), i0.ɵɵdirectiveInject(i2.ToastService), i0.ɵɵdirectiveInject(i3.ConversationAgentService), i0.ɵɵdirectiveInject(i4.ConversationStateService), i0.ɵɵdirectiveInject(i5.DataCacheService), i0.ɵɵdirectiveInject(i6.ActiveTasksService), i0.ɵɵdirectiveInject(i7.MentionAutocompleteService), i0.ɵɵdirectiveInject(i8.MentionParserService)); };
1449
1592
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MessageInputComponent, selectors: [["mj-message-input"]], viewQuery: function MessageInputComponent_Query(rf, ctx) { if (rf & 1) {
1450
1593
  i0.ɵɵviewQuery(_c0, 5);
1451
1594
  } if (rf & 2) {
1452
1595
  let _t;
1453
1596
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.messageTextarea = _t.first);
1454
- } }, inputs: { conversationId: "conversationId", currentUser: "currentUser", disabled: "disabled", placeholder: "placeholder", parentMessageId: "parentMessageId", conversationHistory: "conversationHistory" }, outputs: { messageSent: "messageSent", agentResponse: "agentResponse", agentRunDetected: "agentRunDetected", artifactCreated: "artifactCreated", conversationRenamed: "conversationRenamed" }, decls: 11, vars: 11, consts: [["messageTextarea", ""], [1, "message-input-container"], ["rows", "3", 1, "message-input", 3, "ngModelChange", "keydown", "input", "ngModel", "placeholder", "disabled"], [3, "suggestionSelected", "closed", "suggestions", "position", "visible", "showAbove"], [1, "input-actions"], ["class", "processing-indicator", 4, "ngIf"], ["title", "Attach file (coming soon)", 1, "btn-attach", 3, "disabled"], [1, "fas", "fa-paperclip"], [1, "btn-send", 3, "click", "disabled", "title"], [1, "fas", "fa-paper-plane"], [1, "processing-indicator"], [1, "fas", "fa-circle-notch", "fa-spin"]], template: function MessageInputComponent_Template(rf, ctx) { if (rf & 1) {
1597
+ } }, inputs: { conversationId: "conversationId", currentUser: "currentUser", disabled: "disabled", placeholder: "placeholder", parentMessageId: "parentMessageId", conversationHistory: "conversationHistory" }, outputs: { messageSent: "messageSent", agentResponse: "agentResponse", agentRunDetected: "agentRunDetected", artifactCreated: "artifactCreated", conversationRenamed: "conversationRenamed" }, decls: 11, vars: 13, consts: [["messageTextarea", ""], [1, "message-input-container"], ["rows", "3", 1, "message-input", 3, "ngModelChange", "keydown", "input", "ngModel", "placeholder", "disabled"], [3, "suggestionSelected", "closed", "suggestions", "position", "visible", "showAbove"], [1, "input-actions"], ["class", "processing-indicator", 4, "ngIf"], ["title", "Attach file (coming soon)", 1, "btn-attach", 3, "disabled"], [1, "fas", "fa-paperclip"], [1, "btn-send", 3, "click", "disabled", "title"], [1, "fas", "fa-paper-plane"], [1, "processing-indicator"], [1, "fas", "fa-circle-notch", "fa-spin"]], template: function MessageInputComponent_Template(rf, ctx) { if (rf & 1) {
1455
1598
  const _r1 = i0.ɵɵgetCurrentView();
1456
1599
  i0.ɵɵelementStart(0, "div", 1)(1, "textarea", 2, 0);
1457
1600
  i0.ɵɵtwoWayListener("ngModelChange", function MessageInputComponent_Template_textarea_ngModelChange_1_listener($event) { i0.ɵɵrestoreView(_r1); i0.ɵɵtwoWayBindingSet(ctx.messageText, $event) || (ctx.messageText = $event); return i0.ɵɵresetView($event); });
@@ -1462,7 +1605,7 @@ export class MessageInputComponent {
1462
1605
  i0.ɵɵlistener("suggestionSelected", function MessageInputComponent_Template_mj_mention_dropdown_suggestionSelected_4_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onMentionSelected($event)); })("closed", function MessageInputComponent_Template_mj_mention_dropdown_closed_4_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.closeMentionDropdown()); });
1463
1606
  i0.ɵɵelementEnd();
1464
1607
  i0.ɵɵelementStart(5, "div", 4);
1465
- i0.ɵɵtemplate(6, MessageInputComponent_div_6_Template, 4, 0, "div", 5);
1608
+ i0.ɵɵtemplate(6, MessageInputComponent_div_6_Template, 4, 1, "div", 5);
1466
1609
  i0.ɵɵelementStart(7, "button", 6);
1467
1610
  i0.ɵɵelement(8, "i", 7);
1468
1611
  i0.ɵɵelementEnd();
@@ -1472,8 +1615,9 @@ export class MessageInputComponent {
1472
1615
  i0.ɵɵelementEnd()()();
1473
1616
  } if (rf & 2) {
1474
1617
  i0.ɵɵadvance();
1618
+ i0.ɵɵclassProp("intent-checking", ctx.isProcessing);
1475
1619
  i0.ɵɵtwoWayProperty("ngModel", ctx.messageText);
1476
- i0.ɵɵproperty("placeholder", ctx.placeholder)("disabled", ctx.disabled || ctx.isSending);
1620
+ i0.ɵɵproperty("placeholder", ctx.placeholder)("disabled", ctx.disabled || ctx.isProcessing);
1477
1621
  i0.ɵɵadvance(3);
1478
1622
  i0.ɵɵproperty("suggestions", ctx.mentionSuggestions)("position", ctx.mentionDropdownPosition)("visible", ctx.showMentionDropdown)("showAbove", ctx.mentionDropdownShowAbove);
1479
1623
  i0.ɵɵadvance(2);
@@ -1482,11 +1626,11 @@ export class MessageInputComponent {
1482
1626
  i0.ɵɵproperty("disabled", ctx.disabled);
1483
1627
  i0.ɵɵadvance(2);
1484
1628
  i0.ɵɵproperty("disabled", !ctx.canSend)("title", ctx.isSending ? "Sending..." : "Send message");
1485
- } }, dependencies: [i9.NgIf, i10.DefaultValueAccessor, i10.NgControlStatus, i10.NgModel, i11.MentionDropdownComponent], styles: [".message-input-container[_ngcontent-%COMP%] {\n position: relative;\n padding: 16px 24px;\n border-top: 1px solid #D9D9D9;\n background: #FFF;\n}\n\n.message-input-wrapper[_ngcontent-%COMP%] {\n border: 2px solid #D9D9D9;\n border-radius: 8px;\n padding: 12px;\n transition: border-color 0.2s, box-shadow 0.2s;\n background: #FFF;\n}\n\n.message-input-wrapper[_ngcontent-%COMP%]:focus-within {\n border-color: #0076B6;\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.1);\n}\n\n.message-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 0;\n border: none;\n resize: none;\n font-family: inherit;\n font-size: 14px;\n min-height: 40px;\n max-height: 200px;\n line-height: 1.5;\n}\n\n.message-input[_ngcontent-%COMP%]:focus {\n outline: none;\n}\n\n.message-input[_ngcontent-%COMP%]:disabled {\n background: #F4F4F4;\n cursor: not-allowed;\n}\n.input-actions[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 12px;\n}\n.btn-attach[_ngcontent-%COMP%] {\n padding: 8px 16px;\n background: transparent;\n border: 1px solid #D9D9D9;\n border-radius: 6px;\n cursor: pointer;\n color: #333;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.btn-attach[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #F4F4F4;\n border-color: #AAA;\n}\n.btn-attach[_ngcontent-%COMP%]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.btn-send[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n background: #3B82F6;\n color: white;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.2s;\n flex-shrink: 0;\n}\n.btn-send[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #2563EB;\n}\n.btn-send[_ngcontent-%COMP%]:disabled {\n background: #D9D9D9;\n color: #AAA;\n cursor: not-allowed;\n}\n.btn-send[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 16px;\n}\n.processing-indicator[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #6B7280;\n margin-right: auto;\n}\n.processing-indicator[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #0076B6;\n}"] });
1629
+ } }, dependencies: [i9.NgIf, i10.DefaultValueAccessor, i10.NgControlStatus, i10.NgModel, i11.MentionDropdownComponent], styles: [".message-input-container[_ngcontent-%COMP%] {\n position: relative;\n padding: 16px 24px;\n border-top: 1px solid #D9D9D9;\n background: #FFF;\n}\n\n.message-input-wrapper[_ngcontent-%COMP%] {\n border: 2px solid #D9D9D9;\n border-radius: 8px;\n padding: 12px;\n transition: border-color 0.2s, box-shadow 0.2s;\n background: #FFF;\n}\n\n.message-input-wrapper[_ngcontent-%COMP%]:focus-within {\n border-color: #0076B6;\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.1);\n}\n\n.message-input[_ngcontent-%COMP%] {\n width: 100%;\n padding: 0;\n border: none;\n resize: none;\n font-family: inherit;\n font-size: 14px;\n min-height: 40px;\n max-height: 200px;\n line-height: 1.5;\n}\n\n.message-input[_ngcontent-%COMP%]:focus {\n outline: none;\n}\n\n.message-input[_ngcontent-%COMP%]:disabled {\n background: transparent;\n cursor: wait;\n}\n\n//[_ngcontent-%COMP%] Subtle[_ngcontent-%COMP%] visual[_ngcontent-%COMP%] feedback[_ngcontent-%COMP%] when[_ngcontent-%COMP%] checking[_ngcontent-%COMP%] intent[_ngcontent-%COMP%] (no[_ngcontent-%COMP%] ugly[_ngcontent-%COMP%] gray[_ngcontent-%COMP%] background)\n.message-input.intent-checking[_ngcontent-%COMP%] {\n opacity: 0.6;\n transition: opacity 0.2s;\n}\n.input-actions[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 12px;\n}\n.btn-attach[_ngcontent-%COMP%] {\n padding: 8px 16px;\n background: transparent;\n border: 1px solid #D9D9D9;\n border-radius: 6px;\n cursor: pointer;\n color: #333;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.btn-attach[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #F4F4F4;\n border-color: #AAA;\n}\n.btn-attach[_ngcontent-%COMP%]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.btn-send[_ngcontent-%COMP%] {\n width: 40px;\n height: 40px;\n background: #3B82F6;\n color: white;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.2s;\n flex-shrink: 0;\n}\n.btn-send[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #2563EB;\n}\n.btn-send[_ngcontent-%COMP%]:disabled {\n background: #D9D9D9;\n color: #AAA;\n cursor: not-allowed;\n}\n.btn-send[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 16px;\n}\n.processing-indicator[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #6B7280;\n margin-right: auto;\n}\n.processing-indicator[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #0076B6;\n}"] });
1486
1630
  }
1487
1631
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MessageInputComponent, [{
1488
1632
  type: Component,
1489
- args: [{ selector: 'mj-message-input', template: "<div class=\"message-input-container\">\n <textarea\n #messageTextarea\n class=\"message-input\"\n [(ngModel)]=\"messageText\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled || isSending\"\n (keydown)=\"onKeyDown($event)\"\n (input)=\"onInput($event)\"\n rows=\"3\">\n </textarea>\n\n <!-- Mention Autocomplete Dropdown -->\n <mj-mention-dropdown\n [suggestions]=\"mentionSuggestions\"\n [position]=\"mentionDropdownPosition\"\n [visible]=\"showMentionDropdown\"\n [showAbove]=\"mentionDropdownShowAbove\"\n (suggestionSelected)=\"onMentionSelected($event)\"\n (closed)=\"closeMentionDropdown()\">\n </mj-mention-dropdown>\n\n <div class=\"input-actions\">\n <div class=\"processing-indicator\" *ngIf=\"isProcessing\">\n <i class=\"fas fa-circle-notch fa-spin\"></i>\n <span>AI is responding...</span>\n </div>\n <button\n class=\"btn-attach\"\n [disabled]=\"disabled\"\n title=\"Attach file (coming soon)\">\n <i class=\"fas fa-paperclip\"></i>\n </button>\n <button\n class=\"btn-send\"\n [disabled]=\"!canSend\"\n (click)=\"onSend()\"\n [title]=\"isSending ? 'Sending...' : 'Send message'\">\n <i class=\"fas fa-paper-plane\"></i>\n </button>\n </div>\n</div>", styles: [".message-input-container {\n position: relative;\n padding: 16px 24px;\n border-top: 1px solid #D9D9D9;\n background: #FFF;\n}\n\n.message-input-wrapper {\n border: 2px solid #D9D9D9;\n border-radius: 8px;\n padding: 12px;\n transition: border-color 0.2s, box-shadow 0.2s;\n background: #FFF;\n}\n\n.message-input-wrapper:focus-within {\n border-color: #0076B6;\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.1);\n}\n\n.message-input {\n width: 100%;\n padding: 0;\n border: none;\n resize: none;\n font-family: inherit;\n font-size: 14px;\n min-height: 40px;\n max-height: 200px;\n line-height: 1.5;\n}\n\n.message-input:focus {\n outline: none;\n}\n\n.message-input:disabled {\n background: #F4F4F4;\n cursor: not-allowed;\n}\n.input-actions {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 12px;\n}\n.btn-attach {\n padding: 8px 16px;\n background: transparent;\n border: 1px solid #D9D9D9;\n border-radius: 6px;\n cursor: pointer;\n color: #333;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.btn-attach:hover:not(:disabled) {\n background: #F4F4F4;\n border-color: #AAA;\n}\n.btn-attach:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.btn-send {\n width: 40px;\n height: 40px;\n background: #3B82F6;\n color: white;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.2s;\n flex-shrink: 0;\n}\n.btn-send:hover:not(:disabled) {\n background: #2563EB;\n}\n.btn-send:disabled {\n background: #D9D9D9;\n color: #AAA;\n cursor: not-allowed;\n}\n.btn-send i {\n font-size: 16px;\n}\n.processing-indicator {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #6B7280;\n margin-right: auto;\n}\n.processing-indicator i {\n color: #0076B6;\n}"] }]
1633
+ args: [{ selector: 'mj-message-input', template: "<div class=\"message-input-container\">\n <textarea\n #messageTextarea\n class=\"message-input\"\n [class.intent-checking]=\"isProcessing\"\n [(ngModel)]=\"messageText\"\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled || isProcessing\"\n (keydown)=\"onKeyDown($event)\"\n (input)=\"onInput($event)\"\n rows=\"3\">\n </textarea>\n\n <!-- Mention Autocomplete Dropdown -->\n <mj-mention-dropdown\n [suggestions]=\"mentionSuggestions\"\n [position]=\"mentionDropdownPosition\"\n [visible]=\"showMentionDropdown\"\n [showAbove]=\"mentionDropdownShowAbove\"\n (suggestionSelected)=\"onMentionSelected($event)\"\n (closed)=\"closeMentionDropdown()\">\n </mj-mention-dropdown>\n\n <div class=\"input-actions\">\n <div class=\"processing-indicator\" *ngIf=\"isProcessing\">\n <i class=\"fas fa-circle-notch fa-spin\"></i>\n <span>{{ processingMessage }}</span>\n </div>\n <button\n class=\"btn-attach\"\n [disabled]=\"disabled\"\n title=\"Attach file (coming soon)\">\n <i class=\"fas fa-paperclip\"></i>\n </button>\n <button\n class=\"btn-send\"\n [disabled]=\"!canSend\"\n (click)=\"onSend()\"\n [title]=\"isSending ? 'Sending...' : 'Send message'\">\n <i class=\"fas fa-paper-plane\"></i>\n </button>\n </div>\n</div>", styles: [".message-input-container {\n position: relative;\n padding: 16px 24px;\n border-top: 1px solid #D9D9D9;\n background: #FFF;\n}\n\n.message-input-wrapper {\n border: 2px solid #D9D9D9;\n border-radius: 8px;\n padding: 12px;\n transition: border-color 0.2s, box-shadow 0.2s;\n background: #FFF;\n}\n\n.message-input-wrapper:focus-within {\n border-color: #0076B6;\n box-shadow: 0 0 0 3px rgba(0, 118, 182, 0.1);\n}\n\n.message-input {\n width: 100%;\n padding: 0;\n border: none;\n resize: none;\n font-family: inherit;\n font-size: 14px;\n min-height: 40px;\n max-height: 200px;\n line-height: 1.5;\n}\n\n.message-input:focus {\n outline: none;\n}\n\n.message-input:disabled {\n background: transparent;\n cursor: wait;\n}\n\n// Subtle visual feedback when checking intent (no ugly gray background)\n.message-input.intent-checking {\n opacity: 0.6;\n transition: opacity 0.2s;\n}\n.input-actions {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 12px;\n}\n.btn-attach {\n padding: 8px 16px;\n background: transparent;\n border: 1px solid #D9D9D9;\n border-radius: 6px;\n cursor: pointer;\n color: #333;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.btn-attach:hover:not(:disabled) {\n background: #F4F4F4;\n border-color: #AAA;\n}\n.btn-attach:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.btn-send {\n width: 40px;\n height: 40px;\n background: #3B82F6;\n color: white;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.2s;\n flex-shrink: 0;\n}\n.btn-send:hover:not(:disabled) {\n background: #2563EB;\n}\n.btn-send:disabled {\n background: #D9D9D9;\n color: #AAA;\n cursor: not-allowed;\n}\n.btn-send i {\n font-size: 16px;\n}\n.processing-indicator {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: #6B7280;\n margin-right: auto;\n}\n.processing-indicator i {\n color: #0076B6;\n}"] }]
1490
1634
  }], () => [{ type: i1.DialogService }, { type: i2.ToastService }, { type: i3.ConversationAgentService }, { type: i4.ConversationStateService }, { type: i5.DataCacheService }, { type: i6.ActiveTasksService }, { type: i7.MentionAutocompleteService }, { type: i8.MentionParserService }], { conversationId: [{
1491
1635
  type: Input
1492
1636
  }], currentUser: [{