@memberjunction/ng-conversations 6.2.0-edge.5 → 6.2.0-edge.7

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 (164) hide show
  1. package/dist/lib/components/attachment/image-viewer.component.d.ts +3 -1
  2. package/dist/lib/components/attachment/image-viewer.component.d.ts.map +1 -1
  3. package/dist/lib/components/attachment/image-viewer.component.js +5 -1
  4. package/dist/lib/components/attachment/image-viewer.component.js.map +1 -1
  5. package/dist/lib/components/collection/collection-view.component.d.ts.map +1 -1
  6. package/dist/lib/components/collection/collection-view.component.js +1 -0
  7. package/dist/lib/components/collection/collection-view.component.js.map +1 -1
  8. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts +188 -9
  9. package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
  10. package/dist/lib/components/conversation/conversation-chat-area.component.js +1333 -464
  11. package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
  12. package/dist/lib/components/conversation/conversation-mode-picker.component.js +2 -2
  13. package/dist/lib/components/conversation/conversation-mode-picker.component.js.map +1 -1
  14. package/dist/lib/components/conversation/conversation-remote-browser-controller.d.ts +109 -0
  15. package/dist/lib/components/conversation/conversation-remote-browser-controller.d.ts.map +1 -0
  16. package/dist/lib/components/conversation/conversation-remote-browser-controller.js +298 -0
  17. package/dist/lib/components/conversation/conversation-remote-browser-controller.js.map +1 -0
  18. package/dist/lib/components/message/agent-handoff-card.component.d.ts +102 -0
  19. package/dist/lib/components/message/agent-handoff-card.component.d.ts.map +1 -0
  20. package/dist/lib/components/message/agent-handoff-card.component.js +439 -0
  21. package/dist/lib/components/message/agent-handoff-card.component.js.map +1 -0
  22. package/dist/lib/components/message/message-input.component.d.ts +126 -59
  23. package/dist/lib/components/message/message-input.component.d.ts.map +1 -1
  24. package/dist/lib/components/message/message-input.component.js +559 -554
  25. package/dist/lib/components/message/message-input.component.js.map +1 -1
  26. package/dist/lib/components/message/message-item.component.d.ts +34 -2
  27. package/dist/lib/components/message/message-item.component.d.ts.map +1 -1
  28. package/dist/lib/components/message/message-item.component.js +678 -520
  29. package/dist/lib/components/message/message-item.component.js.map +1 -1
  30. package/dist/lib/components/message/message-list.component.d.ts +13 -1
  31. package/dist/lib/components/message/message-list.component.d.ts.map +1 -1
  32. package/dist/lib/components/message/message-list.component.js +92 -39
  33. package/dist/lib/components/message/message-list.component.js.map +1 -1
  34. package/dist/lib/components/overlay/chat-overlay.component.d.ts +19 -0
  35. package/dist/lib/components/overlay/chat-overlay.component.d.ts.map +1 -1
  36. package/dist/lib/components/overlay/chat-overlay.component.js +125 -13
  37. package/dist/lib/components/overlay/chat-overlay.component.js.map +1 -1
  38. package/dist/lib/components/project/project-form-modal.component.js +2 -2
  39. package/dist/lib/components/project/project-form-modal.component.js.map +1 -1
  40. package/dist/lib/components/realtime/interactive-component/run-view-component-artifact-source.js +1 -1
  41. package/dist/lib/components/realtime/interactive-component/run-view-component-artifact-source.js.map +1 -1
  42. package/dist/lib/components/realtime/media/media-channel-state.d.ts +5 -0
  43. package/dist/lib/components/realtime/media/media-channel-state.d.ts.map +1 -1
  44. package/dist/lib/components/realtime/media/media-channel-state.js +27 -1
  45. package/dist/lib/components/realtime/media/media-channel-state.js.map +1 -1
  46. package/dist/lib/components/realtime/media/media-channel.d.ts.map +1 -1
  47. package/dist/lib/components/realtime/media/media-channel.js +7 -2
  48. package/dist/lib/components/realtime/media/media-channel.js.map +1 -1
  49. package/dist/lib/components/realtime/media/realtime-media-surface.component.d.ts +19 -2
  50. package/dist/lib/components/realtime/media/realtime-media-surface.component.d.ts.map +1 -1
  51. package/dist/lib/components/realtime/media/realtime-media-surface.component.js +59 -10
  52. package/dist/lib/components/realtime/media/realtime-media-surface.component.js.map +1 -1
  53. package/dist/lib/components/realtime/realtime-agent-banner.component.d.ts +22 -6
  54. package/dist/lib/components/realtime/realtime-agent-banner.component.d.ts.map +1 -1
  55. package/dist/lib/components/realtime/realtime-agent-banner.component.js +160 -98
  56. package/dist/lib/components/realtime/realtime-agent-banner.component.js.map +1 -1
  57. package/dist/lib/components/realtime/realtime-call-failure-notice.d.ts +13 -0
  58. package/dist/lib/components/realtime/realtime-call-failure-notice.d.ts.map +1 -0
  59. package/dist/lib/components/realtime/realtime-call-failure-notice.js +21 -0
  60. package/dist/lib/components/realtime/realtime-call-failure-notice.js.map +1 -0
  61. package/dist/lib/components/realtime/realtime-composer.component.d.ts +26 -3
  62. package/dist/lib/components/realtime/realtime-composer.component.d.ts.map +1 -1
  63. package/dist/lib/components/realtime/realtime-composer.component.js +226 -120
  64. package/dist/lib/components/realtime/realtime-composer.component.js.map +1 -1
  65. package/dist/lib/components/realtime/realtime-session-overlay.component.d.ts +73 -16
  66. package/dist/lib/components/realtime/realtime-session-overlay.component.d.ts.map +1 -1
  67. package/dist/lib/components/realtime/realtime-session-overlay.component.js +219 -108
  68. package/dist/lib/components/realtime/realtime-session-overlay.component.js.map +1 -1
  69. package/dist/lib/components/realtime/realtime-ui-config.d.ts +9 -12
  70. package/dist/lib/components/realtime/realtime-ui-config.d.ts.map +1 -1
  71. package/dist/lib/components/realtime/realtime-ui-config.js +38 -18
  72. package/dist/lib/components/realtime/realtime-ui-config.js.map +1 -1
  73. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.d.ts.map +1 -1
  74. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.js +34 -5
  75. package/dist/lib/components/realtime/remote-browser/remote-browser-channel.js.map +1 -1
  76. package/dist/lib/components/realtime/remote-browser/remote-browser-tools.d.ts +8 -2
  77. package/dist/lib/components/realtime/remote-browser/remote-browser-tools.d.ts.map +1 -1
  78. package/dist/lib/components/realtime/remote-browser/remote-browser-tools.js +51 -10
  79. package/dist/lib/components/realtime/remote-browser/remote-browser-tools.js.map +1 -1
  80. package/dist/lib/components/realtime/whiteboard/whiteboard-channel-config.d.ts +46 -0
  81. package/dist/lib/components/realtime/whiteboard/whiteboard-channel-config.d.ts.map +1 -0
  82. package/dist/lib/components/realtime/whiteboard/whiteboard-channel-config.js +64 -0
  83. package/dist/lib/components/realtime/whiteboard/whiteboard-channel-config.js.map +1 -0
  84. package/dist/lib/components/realtime/whiteboard/whiteboard-channel.d.ts +131 -9
  85. package/dist/lib/components/realtime/whiteboard/whiteboard-channel.d.ts.map +1 -1
  86. package/dist/lib/components/realtime/whiteboard/whiteboard-channel.js +347 -38
  87. package/dist/lib/components/realtime/whiteboard/whiteboard-channel.js.map +1 -1
  88. package/dist/lib/conversations.module.d.ts +2 -1
  89. package/dist/lib/conversations.module.d.ts.map +1 -1
  90. package/dist/lib/conversations.module.js +9 -4
  91. package/dist/lib/conversations.module.js.map +1 -1
  92. package/dist/lib/events/chat-events.d.ts +5 -4
  93. package/dist/lib/events/chat-events.d.ts.map +1 -1
  94. package/dist/lib/events/chat-events.js +2 -2
  95. package/dist/lib/events/chat-events.js.map +1 -1
  96. package/dist/lib/models/agent-turn.model.d.ts +11 -7
  97. package/dist/lib/models/agent-turn.model.d.ts.map +1 -1
  98. package/dist/lib/models/agent-turn.model.js.map +1 -1
  99. package/dist/lib/services/active-tasks.service.d.ts +5 -0
  100. package/dist/lib/services/active-tasks.service.d.ts.map +1 -1
  101. package/dist/lib/services/active-tasks.service.js +16 -0
  102. package/dist/lib/services/active-tasks.service.js.map +1 -1
  103. package/dist/lib/services/agent-state.service.d.ts.map +1 -1
  104. package/dist/lib/services/agent-state.service.js +7 -1
  105. package/dist/lib/services/agent-state.service.js.map +1 -1
  106. package/dist/lib/services/artifact-permission.service.d.ts.map +1 -1
  107. package/dist/lib/services/artifact-permission.service.js +2 -0
  108. package/dist/lib/services/artifact-permission.service.js.map +1 -1
  109. package/dist/lib/services/artifact-state.service.d.ts.map +1 -1
  110. package/dist/lib/services/artifact-state.service.js +1 -0
  111. package/dist/lib/services/artifact-state.service.js.map +1 -1
  112. package/dist/lib/services/collection-permission.service.d.ts.map +1 -1
  113. package/dist/lib/services/collection-permission.service.js +1 -0
  114. package/dist/lib/services/collection-permission.service.js.map +1 -1
  115. package/dist/lib/services/conversation-agent.service.d.ts +38 -16
  116. package/dist/lib/services/conversation-agent.service.d.ts.map +1 -1
  117. package/dist/lib/services/conversation-agent.service.js +57 -71
  118. package/dist/lib/services/conversation-agent.service.js.map +1 -1
  119. package/dist/lib/services/conversation-detail-window.store.d.ts +5 -0
  120. package/dist/lib/services/conversation-detail-window.store.d.ts.map +1 -1
  121. package/dist/lib/services/conversation-detail-window.store.js +52 -5
  122. package/dist/lib/services/conversation-detail-window.store.js.map +1 -1
  123. package/dist/lib/services/realtime-session-review.service.d.ts +2 -1
  124. package/dist/lib/services/realtime-session-review.service.d.ts.map +1 -1
  125. package/dist/lib/services/realtime-session-review.service.js.map +1 -1
  126. package/dist/lib/services/realtime-session.service.d.ts.map +1 -1
  127. package/dist/lib/services/realtime-session.service.js +2 -1
  128. package/dist/lib/services/realtime-session.service.js.map +1 -1
  129. package/dist/lib/services/search.service.d.ts.map +1 -1
  130. package/dist/lib/services/search.service.js +1 -0
  131. package/dist/lib/services/search.service.js.map +1 -1
  132. package/dist/lib/utils/agent-failure-display.d.ts +14 -0
  133. package/dist/lib/utils/agent-failure-display.d.ts.map +1 -0
  134. package/dist/lib/utils/agent-failure-display.js +26 -0
  135. package/dist/lib/utils/agent-failure-display.js.map +1 -0
  136. package/dist/lib/utils/agent-handoff-map.d.ts +52 -0
  137. package/dist/lib/utils/agent-handoff-map.d.ts.map +1 -0
  138. package/dist/lib/utils/agent-handoff-map.js +270 -0
  139. package/dist/lib/utils/agent-handoff-map.js.map +1 -0
  140. package/dist/lib/utils/agent-turn-routing.d.ts +62 -7
  141. package/dist/lib/utils/agent-turn-routing.d.ts.map +1 -1
  142. package/dist/lib/utils/agent-turn-routing.js +147 -6
  143. package/dist/lib/utils/agent-turn-routing.js.map +1 -1
  144. package/dist/lib/utils/decision-routing.d.ts +17 -9
  145. package/dist/lib/utils/decision-routing.d.ts.map +1 -1
  146. package/dist/lib/utils/decision-routing.js +23 -14
  147. package/dist/lib/utils/decision-routing.js.map +1 -1
  148. package/dist/lib/utils/message-list-order.d.ts +13 -0
  149. package/dist/lib/utils/message-list-order.d.ts.map +1 -0
  150. package/dist/lib/utils/message-list-order.js +31 -0
  151. package/dist/lib/utils/message-list-order.js.map +1 -0
  152. package/dist/lib/utils/pin-state.d.ts +11 -0
  153. package/dist/lib/utils/pin-state.d.ts.map +1 -0
  154. package/dist/lib/utils/pin-state.js +9 -0
  155. package/dist/lib/utils/pin-state.js.map +1 -0
  156. package/dist/lib/utils/thread-replies.d.ts +34 -0
  157. package/dist/lib/utils/thread-replies.d.ts.map +1 -0
  158. package/dist/lib/utils/thread-replies.js +49 -0
  159. package/dist/lib/utils/thread-replies.js.map +1 -0
  160. package/dist/public-api.d.ts +6 -0
  161. package/dist/public-api.d.ts.map +1 -1
  162. package/dist/public-api.js +6 -0
  163. package/dist/public-api.js.map +1 -1
  164. package/package.json +36 -36
@@ -1,20 +1,21 @@
1
1
  import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
2
2
  import { BaseAngularComponent } from '@memberjunction/ng-base-types';
3
3
  import { LogStatusEx } from '@memberjunction/core';
4
- import { MJEnvironmentEntityExtended, ConversationEngine, UserInfoEngine, TaskGraphSubmitOperation } from '@memberjunction/core-entities';
4
+ import { ConversationEngine, UserInfoEngine } from '@memberjunction/core-entities';
5
5
  import { BeforeAgentTurnEventArgs, AfterAgentTurnEventArgs } from '../../events/chat-events';
6
- import { ResolveAgentTurn, FindDisallowedTaskGraphAgents } from '../../utils/agent-turn-routing';
6
+ import { IsAddressedToHuman, ResolveAgentTurn, FindContinuityAgentMessage } from '../../utils/agent-turn-routing';
7
+ import { AgentAccentVar } from './agent-handoff-card.component';
7
8
  import { ApplyRoutingDecision, ArtifactVersionForTurn, RunRoutingDecision, ShouldRunRoutingDecision } from '../../utils/decision-routing';
8
9
  import { BuildRecentTurns, BuildRoutingArtifactVersions, CanAskRoutingDecision, CollectRoutingParticipants, IsAgentAllowed } from '@memberjunction/ai-core-plus';
9
10
  import { GenerateAndApplyConversationName } from '../../services/conversation-naming';
10
- import { AIEngineBase } from '@memberjunction/ai-engine-base';
11
- import { ConversationUtility, agentFailureDisposition, agentFailureMessage } from '@memberjunction/ai-core-plus';
11
+ import { AIEngineBase, ResolveConversationAgentPreset } from '@memberjunction/ai-engine-base';
12
+ import { ConversationUtility, agentFailureDisposition, agentFailureMessage, ContinuationAnnouncementText } from '@memberjunction/ai-core-plus';
12
13
  import { MentionAutocompleteService } from '../../services/mention-autocomplete.service';
13
14
  import { PlanModePreference } from '../../utils/plan-mode-preference';
14
15
  import { MJNotificationService } from '@memberjunction/ng-notifications';
15
16
  import { ConversationCallStartOptions } from '../realtime/capture/realtime-capture-channel';
16
17
  import { BuildRealtimeConfigOverridesJson, FilterRealtimeCoAgents, LoadCoAgentPairings, PairingsAllowTarget } from '../../services/realtime-pairing';
17
- import { UUIDsEqual } from '@memberjunction/global';
18
+ import { IndexByUUID, UUIDSet, UUIDsEqual } from '@memberjunction/global';
18
19
  import { InjectFrameZone } from '../../util/frame-zone';
19
20
  import * as i0 from "@angular/core";
20
21
  import * as i1 from "../../services/dialog.service";
@@ -33,7 +34,7 @@ import * as i13 from "../composer/ai-composer.component";
33
34
  const _c0 = ["inputBox"];
34
35
  function MessageInputComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
35
36
  i0.ɵɵelementStart(0, "div", 3);
36
- i0.ɵɵelement(1, "i", 6);
37
+ i0.ɵɵelement(1, "i", 7);
37
38
  i0.ɵɵelementStart(2, "span");
38
39
  i0.ɵɵtext(3);
39
40
  i0.ɵɵelementEnd()();
@@ -42,10 +43,39 @@ function MessageInputComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
42
43
  i0.ɵɵadvance(3);
43
44
  i0.ɵɵtextInterpolate(ctx_r0.processingMessage);
44
45
  } }
45
- function MessageInputComponent_ng_template_5_Template(rf, ctx) { if (rf & 1) {
46
+ function MessageInputComponent_Conditional_3_Conditional_7_Template(rf, ctx) { if (rf & 1) {
46
47
  const _r2 = i0.ɵɵgetCurrentView();
47
- i0.ɵɵelementStart(0, "mj-realtime-agent-picker", 7);
48
- i0.ɵɵlistener("AgentPicked", function MessageInputComponent_ng_template_5_Template_mj_realtime_agent_picker_AgentPicked_0_listener($event) { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onRealtimeAgentPicked($event)); })("Cancelled", function MessageInputComponent_ng_template_5_Template_mj_realtime_agent_picker_Cancelled_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onRealtimeAgentPickerCancelled()); });
48
+ i0.ɵɵelementStart(0, "button", 12);
49
+ i0.ɵɵlistener("click", function MessageInputComponent_Conditional_3_Conditional_7_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.ReturnToSage()); });
50
+ i0.ɵɵtext(1);
51
+ i0.ɵɵelementEnd();
52
+ } if (rf & 2) {
53
+ const ctx_r0 = i0.ɵɵnextContext(2);
54
+ i0.ɵɵadvance();
55
+ i0.ɵɵtextInterpolate1(" Return to ", (ctx_r0.ConverationManagerAgent == null ? null : ctx_r0.ConverationManagerAgent.Name) || "Sage", " ");
56
+ } }
57
+ function MessageInputComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
58
+ i0.ɵɵelementStart(0, "div", 8)(1, "span", 9);
59
+ i0.ɵɵelement(2, "span", 10);
60
+ i0.ɵɵelementStart(3, "span");
61
+ i0.ɵɵtext(4, "Replying to ");
62
+ i0.ɵɵelementStart(5, "b");
63
+ i0.ɵɵtext(6);
64
+ i0.ɵɵelementEnd()()();
65
+ i0.ɵɵconditionalCreate(7, MessageInputComponent_Conditional_3_Conditional_7_Template, 2, 1, "button", 11);
66
+ i0.ɵɵelementEnd();
67
+ } if (rf & 2) {
68
+ const ctx_r0 = i0.ɵɵnextContext();
69
+ i0.ɵɵstyleProp("--agent-accent", ctx_r0.CurrentRouteAccentVar);
70
+ i0.ɵɵadvance(6);
71
+ i0.ɵɵtextInterpolate(ctx_r0.CurrentRouteAgentName);
72
+ i0.ɵɵadvance();
73
+ i0.ɵɵconditional(ctx_r0.ShowReturnToSage ? 7 : -1);
74
+ } }
75
+ function MessageInputComponent_ng_template_6_Template(rf, ctx) { if (rf & 1) {
76
+ const _r3 = i0.ɵɵgetCurrentView();
77
+ i0.ɵɵelementStart(0, "mj-realtime-agent-picker", 13);
78
+ i0.ɵɵlistener("AgentPicked", function MessageInputComponent_ng_template_6_Template_mj_realtime_agent_picker_AgentPicked_0_listener($event) { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onRealtimeAgentPicked($event)); })("Cancelled", function MessageInputComponent_ng_template_6_Template_mj_realtime_agent_picker_Cancelled_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onRealtimeAgentPickerCancelled()); });
49
79
  i0.ɵɵelementEnd();
50
80
  } if (rf & 2) {
51
81
  const ctx_r0 = i0.ɵɵnextContext();
@@ -824,7 +854,7 @@ export class MessageInputComponent extends BaseAngularComponent {
824
854
  */
825
855
  async InsertAgentMention(agentName, focus = true, clearExisting = true) {
826
856
  if (!this.InputBox) {
827
- console.log(`[Omnibar→Chat] InsertAgentMention('${agentName}'): input box not mounted yet — caller will retry`);
857
+ LogStatusEx({ message: `[Omnibar→Chat] InsertAgentMention('${agentName}'): input box not mounted yet — caller will retry`, verboseOnly: true });
828
858
  return false;
829
859
  }
830
860
  if (clearExisting) {
@@ -835,7 +865,7 @@ export class MessageInputComponent extends BaseAngularComponent {
835
865
  }
836
866
  try {
837
867
  if (!this.mentionAutocomplete.IsInitialized && this.CurrentUser) {
838
- console.log(`[Omnibar→Chat] InsertAgentMention('${agentName}'): initializing mention autocomplete…`);
868
+ LogStatusEx({ message: `[Omnibar→Chat] InsertAgentMention('${agentName}'): initializing mention autocomplete…`, verboseOnly: true });
839
869
  await this.mentionAutocomplete.initialize(this.CurrentUser);
840
870
  }
841
871
  const wanted = agentName.trim().toLowerCase();
@@ -844,7 +874,7 @@ export class MessageInputComponent extends BaseAngularComponent {
844
874
  .find(s => s.type === 'agent' && s.name.trim().toLowerCase() === wanted);
845
875
  if (suggestion) {
846
876
  const inserted = this.InputBox.InsertMention(suggestion, focus);
847
- console.log(`[Omnibar→Chat] InsertAgentMention('${agentName}'): resolved to pill (id=${suggestion.id}) — insert ${inserted ? 'OK' : 'FAILED (editor view not ready)'}`);
877
+ LogStatusEx({ message: `[Omnibar→Chat] InsertAgentMention('${agentName}'): resolved to pill (id=${suggestion.id}) — insert ${inserted ? 'OK' : 'FAILED (editor view not ready)'}`, verboseOnly: true });
848
878
  if (inserted) {
849
879
  if (focus) {
850
880
  this.scheduleFocusReassert(agentName);
@@ -873,7 +903,7 @@ export class MessageInputComponent extends BaseAngularComponent {
873
903
  const editor = this.InputBox?.mentionEditor;
874
904
  if (editor && !editor.HasFocus) {
875
905
  const ok = editor.FocusCaretAtEnd();
876
- console.log(`[Omnibar→Chat] focus re-assert (+${delay}ms) for '${context}': ${ok ? 'refocused' : 'editor gone'}`);
906
+ LogStatusEx({ message: `[Omnibar→Chat] focus re-assert (+${delay}ms) for '${context}': ${ok ? 'refocused' : 'editor gone'}`, verboseOnly: true });
877
907
  }
878
908
  }, delay);
879
909
  }
@@ -980,6 +1010,21 @@ export class MessageInputComponent extends BaseAngularComponent {
980
1010
  // ── Voice session (Realtime Co-Agent) ───────────────────────────────
981
1011
  /** True while a live voice session is active — drives the overlay + mic state. */
982
1012
  VoiceActive = false;
1013
+ /** True when THIS composer instance initiated the currently-open realtime call. */
1014
+ startedRealtimeSession = false;
1015
+ /**
1016
+ * True when this composer owns active work or unsent attachments that would be
1017
+ * disrupted or lost if the parent chat area unmounted this hidden instance.
1018
+ */
1019
+ get HasActiveWork() {
1020
+ return Boolean(this.IsSending ||
1021
+ this.IsProcessing ||
1022
+ this.IsUploadingAttachments ||
1023
+ (this.registeredCallbacks?.size ?? 0) > 0 ||
1024
+ (this.inFlightWatches?.size ?? 0) > 0 ||
1025
+ (this.pendingAttachments?.length ?? 0) > 0 ||
1026
+ (this.ConversationId && this.realtimeSession?.IsActiveFor?.(this.ConversationId)));
1027
+ }
983
1028
  /** @deprecated Use {@link VoiceActive}. */
984
1029
  get voiceActive() {
985
1030
  return this.VoiceActive;
@@ -1000,6 +1045,9 @@ export class MessageInputComponent extends BaseAngularComponent {
1000
1045
  // Reflect the live voice-session Active flag into a local field for the template.
1001
1046
  this.realtimeActiveSub = this.realtimeSession.Active$.subscribe(active => {
1002
1047
  this.VoiceActive = active;
1048
+ if (!active) {
1049
+ this.startedRealtimeSession = false;
1050
+ }
1003
1051
  });
1004
1052
  this.ConverationManagerAgent = await this.agentService.getConversationManagerAgent();
1005
1053
  // Warm UserInfoEngine so the PlanModeEnabled getter has the cached settings available
@@ -1072,8 +1120,15 @@ export class MessageInputComponent extends BaseAngularComponent {
1072
1120
  this.unregisterAllCallbacks();
1073
1121
  this.clearInFlightWatches();
1074
1122
  this.realtimeActiveSub?.unsubscribe();
1075
- // If the user navigates away mid-call, tear the session down.
1076
- if (this.realtimeSession.IsActive) {
1123
+ // Only end a live voice session if THIS composer owns it — cached hidden composers
1124
+ // for older conversations are unmounted on LRU eviction and must not hang up a
1125
+ // minimized call running on another conversation (or one minted from the empty state).
1126
+ const ownsActiveCall = this.ConversationId
1127
+ ? Boolean(this.realtimeSession?.IsActiveFor?.(this.ConversationId))
1128
+ : Boolean(this.startedRealtimeSession &&
1129
+ this.realtimeSession?.IsActive &&
1130
+ !this.realtimeSession.SessionCreatedConversationId);
1131
+ if (ownsActiveCall) {
1077
1132
  void this.realtimeSession.EndRealtimeSession();
1078
1133
  }
1079
1134
  }
@@ -1113,6 +1168,104 @@ export class MessageInputComponent extends BaseAngularComponent {
1113
1168
  get pickerTargetAgentId() {
1114
1169
  return this.PickerTargetAgentId;
1115
1170
  }
1171
+ _cachedRouteAgentKey;
1172
+ _cachedRouteAgent = null;
1173
+ /**
1174
+ * Agent that will receive the next turn based on an explicit `@agent` draft chip
1175
+ * or continuity / default routing ({@link PickerTargetAgentId}).
1176
+ * Memoized by message count and routing inputs so template evaluation does not
1177
+ * scan `ConversationHistory` on every change-detection cycle.
1178
+ */
1179
+ get CurrentRouteAgent() {
1180
+ if (this.mentionedAgentId === undefined) {
1181
+ const chips = this.InputBox?.getMentionChipsData() || [];
1182
+ this.mentionedAgentId = chips.find(chip => chip.type === 'agent')?.id ?? null;
1183
+ }
1184
+ const history = this.ConversationHistory ?? [];
1185
+ const lastRow = history.length > 0 ? history[history.length - 1] : null;
1186
+ const manager = this.ConverationManagerAgent;
1187
+ const floorMs = this.AgentHistoryFrom ? new Date(this.AgentHistoryFrom).getTime() : '';
1188
+ const allowedKey = this.AllowedAgentIDs ? this.AllowedAgentIDs.join(',') : '';
1189
+ const cacheKey = `${history.length}|${lastRow?.ID ?? ''}|${lastRow?.AgentID ?? ''}|${this.mentionedAgentId ?? ''}|${this.ConversationDefaultAgentId ?? ''}|${this.DefaultAgentId ?? ''}|${manager?.ID ?? ''}|${floorMs}|${allowedKey}`;
1190
+ if (this._cachedRouteAgentKey === cacheKey) {
1191
+ return this._cachedRouteAgent ?? null;
1192
+ }
1193
+ const targetId = this.PickerTargetAgentId;
1194
+ let resolved = null;
1195
+ if (!targetId) {
1196
+ if (manager?.ID) {
1197
+ resolved = { ID: manager.ID, Name: manager.Name || 'Sage', IsConversationManager: true };
1198
+ }
1199
+ }
1200
+ else if (manager?.ID && UUIDsEqual(targetId, manager.ID)) {
1201
+ resolved = { ID: manager.ID, Name: manager.Name || 'Sage', IsConversationManager: true };
1202
+ }
1203
+ else {
1204
+ const found = AIEngineBase.Instance.ReadableAgents?.find(a => UUIDsEqual(a.ID, targetId));
1205
+ if (found) {
1206
+ resolved = { ID: found.ID, Name: found.Name || 'Agent', IsConversationManager: false };
1207
+ }
1208
+ else {
1209
+ const chips = this.InputBox?.getMentionChipsData() || [];
1210
+ const chipMatch = chips.find(c => c.type === 'agent' && UUIDsEqual(c.id, targetId));
1211
+ if (chipMatch) {
1212
+ resolved = { ID: targetId, Name: chipMatch.name || 'Agent', IsConversationManager: false };
1213
+ }
1214
+ else {
1215
+ const lastReply = this.findLastNonSageReply();
1216
+ if (lastReply?.AgentID && UUIDsEqual(lastReply.AgentID, targetId)) {
1217
+ resolved = { ID: targetId, Name: lastReply.Agent || 'Agent', IsConversationManager: false };
1218
+ }
1219
+ else {
1220
+ resolved = { ID: targetId, Name: 'Agent', IsConversationManager: false };
1221
+ }
1222
+ }
1223
+ }
1224
+ }
1225
+ this._cachedRouteAgentKey = cacheKey;
1226
+ this._cachedRouteAgent = resolved;
1227
+ return resolved;
1228
+ }
1229
+ get CurrentRouteAgentName() {
1230
+ return this.CurrentRouteAgent?.Name || this.ConverationManagerAgent?.Name || 'Sage';
1231
+ }
1232
+ get CurrentRouteAccentVar() {
1233
+ return AgentAccentVar(this.CurrentRouteAgent?.ID);
1234
+ }
1235
+ get ShowReturnToSage() {
1236
+ const route = this.CurrentRouteAgent;
1237
+ return Boolean(!this.EmptyStateMode && route && !route.IsConversationManager && this.ConverationManagerAgent);
1238
+ }
1239
+ /**
1240
+ * Inserts a `@Sage` mention chip into the composer (preserving any typed draft text)
1241
+ * and focuses the editor so the next turn routes back to the conversation manager.
1242
+ */
1243
+ async ReturnToSage() {
1244
+ const manager = this.ConverationManagerAgent;
1245
+ if (!manager) {
1246
+ return false;
1247
+ }
1248
+ const name = manager.Name || 'Sage';
1249
+ if (this.InputBox) {
1250
+ const inserted = this.InputBox.InsertMention({
1251
+ id: manager.ID,
1252
+ type: 'agent',
1253
+ name,
1254
+ displayName: name,
1255
+ icon: manager.IconClass || undefined,
1256
+ imageUrl: manager.LogoURL || undefined,
1257
+ }, true);
1258
+ if (inserted) {
1259
+ this.mentionedAgentId = manager.ID;
1260
+ return true;
1261
+ }
1262
+ }
1263
+ const prefix = name.includes(' ') ? `@"${name}" ` : `@${name} `;
1264
+ const existing = this.MessageText ?? '';
1265
+ this.SetDraft(`${prefix}${existing}`, true);
1266
+ this.mentionedAgentId = manager.ID;
1267
+ return true;
1268
+ }
1116
1269
  /**
1117
1270
  * Memo for the first `@agent` chip in the draft, so the template-bound
1118
1271
  * {@link pickerTargetAgentId} does not walk the editor DOM on every change-detection cycle.
@@ -1404,6 +1557,7 @@ export class MessageInputComponent extends BaseAngularComponent {
1404
1557
  */
1405
1558
  async startRealtimeWithAgent(agentId, agentName, preferredModelId, coAgentId, configOverridesJson, recordingConsent) {
1406
1559
  try {
1560
+ this.startedRealtimeSession = true;
1407
1561
  await this.realtimeSession.StartRealtimeSession(agentId, this.ConversationId, null, agentName, preferredModelId ?? null, null, coAgentId ?? null, configOverridesJson ?? null, recordingConsent ?? null, null,
1408
1562
  // App awareness: the app the session runs in + the live app-context snapshot (where the
1409
1563
  // user is, what they see, capability manifest) — drives the server-side app cascade + the
@@ -1413,6 +1567,7 @@ export class MessageInputComponent extends BaseAngularComponent {
1413
1567
  { ...ConversationCallStartOptions(), ConversationBranchID: this.TargetBranchID });
1414
1568
  }
1415
1569
  catch (error) {
1570
+ this.startedRealtimeSession = false;
1416
1571
  console.error('Failed to start voice session:', error);
1417
1572
  this.toastService.error('Could not start the voice session.');
1418
1573
  }
@@ -1515,13 +1670,13 @@ export class MessageInputComponent extends BaseAngularComponent {
1515
1670
  }
1516
1671
  // Skip if already complete or errored
1517
1672
  if (message.Status === 'Complete' || message.Status === 'Error') {
1518
- console.log(`[StreamingCallback] Message ${messageId} is ${message.Status}, ignoring progress update`);
1673
+ LogStatusEx({ message: `[StreamingCallback] Message ${messageId} is ${message.Status}, ignoring progress update`, verboseOnly: true });
1519
1674
  return;
1520
1675
  }
1521
1676
  // Check if message was marked as completed (prevents race condition)
1522
1677
  const completionTime = this.completionTimestamps.get(messageId);
1523
1678
  if (completionTime) {
1524
- console.log(`[StreamingCallback] Message ${messageId} marked complete at ${new Date(completionTime).toISOString()}, ignoring late progress update`);
1679
+ LogStatusEx({ message: `[StreamingCallback] Message ${messageId} marked complete at ${new Date(completionTime).toISOString()}, ignoring late progress update`, verboseOnly: true });
1525
1680
  return;
1526
1681
  }
1527
1682
  // Streamed final-response content: the service accumulates deltas, so
@@ -1557,7 +1712,7 @@ export class MessageInputComponent extends BaseAngularComponent {
1557
1712
  this.MessageSent.emit(message);
1558
1713
  // CRITICAL: Update ActiveTasksService to keep the tasks dropdown in sync
1559
1714
  this.activeTasks.updateStatusByConversationDetailId(message.ID, progress.message);
1560
- console.log(`[StreamingCallback] Updated message ${messageId}: ${progress.taskName || 'Agent'}`);
1715
+ LogStatusEx({ message: `[StreamingCallback] Updated message ${messageId}: ${progress.taskName || 'Agent'}`, verboseOnly: true });
1561
1716
  }
1562
1717
  catch (error) {
1563
1718
  console.error(`[StreamingCallback] Error updating message ${messageId}:`, error);
@@ -1572,7 +1727,7 @@ export class MessageInputComponent extends BaseAngularComponent {
1572
1727
  if (this.registeredCallbacks.size === 0) {
1573
1728
  return;
1574
1729
  }
1575
- console.log(`🧹 Unregistering ${this.registeredCallbacks.size} message callbacks`);
1730
+ LogStatusEx({ message: `🧹 Unregistering ${this.registeredCallbacks.size} message callbacks`, verboseOnly: true });
1576
1731
  for (const [messageId, callback] of this.registeredCallbacks) {
1577
1732
  this.streamingService.unregisterMessageCallback(messageId, callback);
1578
1733
  }
@@ -1910,18 +2065,6 @@ export class MessageInputComponent extends BaseAngularComponent {
1910
2065
  return undefined;
1911
2066
  }
1912
2067
  }
1913
- /**
1914
- * A new AI row of an agent turn: in `conversationId`, on the turn's branch `branchId` (null is
1915
- * Main), visible. The caller sets Message, ParentID, Status and AgentID, then saves it.
1916
- */
1917
- async createTurnRow(conversationId, branchId) {
1918
- const row = await this.dataCache.createConversationDetail(this.CurrentUser);
1919
- row.ConversationID = conversationId;
1920
- row.BranchID = branchId;
1921
- row.Role = 'AI';
1922
- row.HiddenToUser = false;
1923
- return row;
1924
- }
1925
2068
  /**
1926
2069
  * Creates and configures a new conversation detail message on `branchId` (null is Main)
1927
2070
  */
@@ -2012,11 +2155,13 @@ export class MessageInputComponent extends BaseAngularComponent {
2012
2155
  // Read the candidates before any await. This composer stays cached while the person opens
2013
2156
  // another conversation, and the chat area then rebinds its history and pinned agent, so a
2014
2157
  // read after the routing decision could see another conversation's state (or none).
2015
- const candidates = this.agentTurnCandidates(this.agentMentionIds(mentionResult));
2158
+ const candidates = this.agentTurnCandidates(this.agentMentionIds(mentionResult), this.userMentionIds(mentionResult), messageDetail.ParentID ?? null, messageDetail.UserID ?? this.CurrentUser?.ID ?? null);
2016
2159
  const routing = await this.decideAgentRouting(messageDetail, candidates, branchId);
2017
- const target = this.resolveAgentTurnTarget(candidates, routing);
2160
+ const appliedCandidates = ApplyRoutingDecision(candidates, routing);
2161
+ const target = this.resolveAgentTurnTarget(appliedCandidates);
2018
2162
  if (!target) {
2019
- await this.finishWithoutAgentTurn(messageDetail, 'NoAgent');
2163
+ const reason = IsAddressedToHuman(appliedCandidates) ? 'AddressedToHuman' : 'NoAgent';
2164
+ await this.finishWithoutAgentTurn(messageDetail, reason);
2020
2165
  return;
2021
2166
  }
2022
2167
  const turn = this.announceAgentTurn(messageDetail, target);
@@ -2040,6 +2185,9 @@ export class MessageInputComponent extends BaseAngularComponent {
2040
2185
  Enabled: this.EnableDecisionRouting,
2041
2186
  ReplyMode: this.AgentReplyMode,
2042
2187
  MentionedAgentIds: candidates.MentionedAgentIds,
2188
+ MentionedUserIds: candidates.MentionedUserIds,
2189
+ ReplyParentAgentId: candidates.ReplyParentAgentId,
2190
+ ReplyParentIsHuman: candidates.ReplyParentIsHuman,
2043
2191
  Message: message.Message ?? '',
2044
2192
  ContinuityAgentId: continuityAgentId
2045
2193
  });
@@ -2052,7 +2200,9 @@ export class MessageInputComponent extends BaseAngularComponent {
2052
2200
  if (!CanAskRoutingDecision(input)) {
2053
2201
  return null;
2054
2202
  }
2055
- input.ArtifactVersions = await this.loadRoutingArtifactVersions(message.ConversationID, branchId, input.Participants);
2203
+ input.ArtifactVersions = this.AgentReplyMode === 'Always'
2204
+ ? await this.loadRoutingArtifactVersions(message.ConversationID, branchId, input.Participants)
2205
+ : [];
2056
2206
  const outcome = await RunRoutingDecision(input, params => this.agentService.RunDecision(params));
2057
2207
  LogStatusEx({
2058
2208
  message: `Decision routing: ${outcome.Verdict}, ${outcome.Reason} (prompt run ${outcome.PromptRunID ?? 'none'})`,
@@ -2070,29 +2220,79 @@ export class MessageInputComponent extends BaseAngularComponent {
2070
2220
  * have answered within the history floor, the last few turns, and the conversation manager.
2071
2221
  * Artifact versions are added after, once the input is known to be worth asking about.
2072
2222
  *
2073
- * Participants come only from the '@' list's agents (`GetAvailableAgents`): active, top-level,
2074
- * unrestricted agents this person has run permission for. An agent that answered here but that
2075
- * the person can't run (a shared conversation, a revoked permission) is never offered, so routing
2076
- * can't send the turn to an agent the server would refuse. Before that list has loaded there are
2077
- * no participants, so no call is made and the message keeps today's routing. Speaker names in
2078
- * the recent turns still come from the full catalog, since any agent may have spoken.
2223
+ * Participants come only from the '@' list's agents (`GetAvailableAgents`) or restricted agents
2224
+ * the host explicitly granted in `AllowedAgentIDs`. An agent that answered here but that the
2225
+ * person can't run (a shared conversation, a revoked permission) is never offered, so routing
2226
+ * can't send the turn to an agent the server would refuse.
2079
2227
  */
2080
2228
  buildRoutingDecisionInput(message, continuityAgentId) {
2081
2229
  const history = this.ConversationHistory.filter(row => this.isWithinHistoryFloor(row) && !UUIDsEqual(row.ID, message.ID));
2082
2230
  const findAgent = (agentId) => AIEngineBase.Instance.ReadableAgents.find(a => UUIDsEqual(a.ID, agentId));
2083
- const runnable = this.mentionAutocomplete.GetAvailableAgents();
2084
- const findRunnable = (agentId) => runnable.find(a => UUIDsEqual(a.ID, agentId));
2231
+ const runnable = (this.mentionAutocomplete ?? MentionAutocompleteService.Instance).GetAvailableAgents();
2232
+ const findRunnable = (agentId) => this.findRoutingRunnableAgent(agentId, runnable, findAgent);
2233
+ const allowNoAgentOption = this.AgentReplyMode === 'MentionOnly';
2234
+ const peopleById = this.MentionPeople?.length ? IndexByUUID(this.MentionPeople) : null;
2235
+ const findPersonName = peopleById
2236
+ ? (userId) => peopleById.Get(userId)?.Name
2237
+ : undefined;
2085
2238
  const manager = this.ConverationManagerAgent;
2239
+ const managerIdToExclude = this.AgentReplyMode === 'Always' ? (manager?.ID ?? null) : null;
2086
2240
  return {
2087
2241
  Message: message.Message ?? '',
2088
2242
  ContinuityAgentId: continuityAgentId,
2089
- Participants: CollectRoutingParticipants(history, manager?.ID ?? null, this.AllowedAgentIDs, findRunnable),
2090
- ConversationManager: manager?.ID && IsAgentAllowed(manager.ID, this.AllowedAgentIDs) ? manager : null,
2091
- RecentTurns: BuildRecentTurns(history, findAgent),
2243
+ Participants: CollectRoutingParticipants(history, managerIdToExclude, this.AllowedAgentIDs, findRunnable),
2244
+ ConversationManager: this.AgentReplyMode === 'Always' && manager?.ID && IsAgentAllowed(manager.ID, this.AllowedAgentIDs) ? manager : null,
2245
+ RecentTurns: BuildRecentTurns(history, findAgent, findPersonName),
2092
2246
  ArtifactVersions: [],
2093
- AllowedAgentIDs: this.AllowedAgentIDs
2247
+ AllowedAgentIDs: this.AllowedAgentIDs,
2248
+ AllowNoAgentOption: allowNoAgentOption
2094
2249
  };
2095
2250
  }
2251
+ findRoutingRunnableAgent(agentId, runnable, findAgent) {
2252
+ const fromAutocomplete = runnable.find(a => UUIDsEqual(a.ID, agentId));
2253
+ if (fromAutocomplete) {
2254
+ return fromAutocomplete;
2255
+ }
2256
+ if (this.AllowedAgentIDs != null && IsAgentAllowed(agentId, this.AllowedAgentIDs)) {
2257
+ const candidate = findAgent(agentId);
2258
+ if (candidate && this.isRunnableRestrictedAgent(candidate)) {
2259
+ return candidate;
2260
+ }
2261
+ }
2262
+ return undefined;
2263
+ }
2264
+ /**
2265
+ * True when a restricted agent (`IsRestricted`) passes the same runnability checks the '@' list
2266
+ * (`MentionAutocomplete._initializeInternal`) applies to unrestricted agents: active, top-level,
2267
+ * not a sub-agent, and runnable by the current user.
2268
+ */
2269
+ isRunnableRestrictedAgent(candidate) {
2270
+ if (!candidate.IsRestricted || candidate.ParentID || candidate.Status !== 'Active' || candidate.InvocationMode === 'Sub-Agent') {
2271
+ return false;
2272
+ }
2273
+ return this.canCurrentUserRunAgent(candidate);
2274
+ }
2275
+ canCurrentUserRunAgent(agent) {
2276
+ const user = this.CurrentUser;
2277
+ if (!user) {
2278
+ return false;
2279
+ }
2280
+ if (UUIDsEqual(agent.OwnerUserID, user.ID)) {
2281
+ return true;
2282
+ }
2283
+ try {
2284
+ const agentPermissions = AIEngineBase.Instance.AgentPermissions.filter(p => UUIDsEqual(p.AgentID, agent.ID));
2285
+ if (agentPermissions.length === 0) {
2286
+ return true;
2287
+ }
2288
+ const userRoleIds = new UUIDSet(user.UserRoles?.map(ur => ur.RoleID) ?? []);
2289
+ return agentPermissions.some(p => (UUIDsEqual(p.UserID, user.ID) || (p.RoleID != null && userRoleIds.Has(p.RoleID))) &&
2290
+ (p.CanDelete === true || p.CanEdit === true || p.CanRun === true));
2291
+ }
2292
+ catch {
2293
+ return false;
2294
+ }
2295
+ }
2096
2296
  /** Every participant's artifact versions on the path of `branchId` in this conversation, for the artifact question. */
2097
2297
  async loadRoutingArtifactVersions(conversationId, branchId, participants) {
2098
2298
  const artifactsByAgent = await Promise.all(participants.map(async (participant) => ({
@@ -2106,10 +2306,18 @@ export class MessageInputComponent extends BaseAngularComponent {
2106
2306
  return { ReplyMode: this.AgentReplyMode, AllowedAgentIDs: this.AllowedAgentIDs };
2107
2307
  }
2108
2308
  /** The agent each route would use for this conversation, before the host's rules apply. */
2109
- agentTurnCandidates(mentionedAgentIds) {
2309
+ agentTurnCandidates(mentionedAgentIds, mentionedUserIds = [], parentId = null, senderUserId = this.CurrentUser?.ID ?? null) {
2310
+ const parentRow = parentId
2311
+ ? this.ConversationHistory.find(row => this.isWithinHistoryFloor(row) && UUIDsEqual(row.ID, parentId))
2312
+ : undefined;
2110
2313
  return {
2111
2314
  MentionedAgentIds: mentionedAgentIds,
2112
- ContinuityAgentId: this.findLastNonSageAgentId(),
2315
+ MentionedUserIds: mentionedUserIds,
2316
+ ReplyParentAgentId: parentRow?.Role === 'AI' ? (parentRow.AgentID ?? null) : null,
2317
+ ReplyParentIsHuman: parentRow?.Role === 'User' &&
2318
+ !!parentRow.UserID &&
2319
+ (!senderUserId || !UUIDsEqual(parentRow.UserID, senderUserId)),
2320
+ ContinuityAgentId: this.AgentReplyMode === 'MentionOnly' ? this.findLastReplyAgentId() : this.findLastNonSageAgentId(),
2113
2321
  ConversationDefaultAgentId: this.ConversationDefaultAgentId,
2114
2322
  HostDefaultAgentId: this.DefaultAgentId,
2115
2323
  ConversationManagerAgentId: this.ConverationManagerAgent?.ID ?? null
@@ -2117,15 +2325,19 @@ export class MessageInputComponent extends BaseAngularComponent {
2117
2325
  }
2118
2326
  /**
2119
2327
  * Picks the agent that answers this message and the route that chose it, or null for no turn.
2120
- * A routing decision, when there was one, is applied to the candidates first.
2121
2328
  */
2122
- resolveAgentTurnTarget(candidates, routing) {
2123
- return ResolveAgentTurn(ApplyRoutingDecision(candidates, routing), this.agentTurnRules, agentId => this.isKnownAgent(agentId));
2329
+ resolveAgentTurnTarget(candidates) {
2330
+ return ResolveAgentTurn(candidates, this.agentTurnRules, agentId => this.isKnownAgent(agentId), agentId => this.isRunnableAgent(agentId));
2124
2331
  }
2125
2332
  /** The agents a message tags, in the order they appear. */
2126
2333
  agentMentionIds(mentionResult) {
2127
2334
  return mentionResult.mentions.filter(m => m.type === 'agent').map(m => m.id);
2128
2335
  }
2336
+ /** The people a message tags (`@Person`), in the order they appear. */
2337
+ userMentionIds(mentionResult) {
2338
+ const fromMentions = mentionResult.mentions.filter(m => m.type === 'user').map(m => m.id);
2339
+ return fromMentions.length > 0 ? fromMentions : (mentionResult.userMentions ?? []).map(m => m.id);
2340
+ }
2129
2341
  /**
2130
2342
  * Fires {@link BeforeAgentTurn} for the turn routing picked — before any reply row exists —
2131
2343
  * and applies what its listeners decided. Returns the turn to run, or null when a listener
@@ -2149,9 +2361,9 @@ export class MessageInputComponent extends BaseAngularComponent {
2149
2361
  return { AgentId: redirectAgentId, Route: 'Redirect' };
2150
2362
  }
2151
2363
  /**
2152
- * Settles a message that starts no agent turn — `MentionOnly` with no tagged agent, no allowed
2153
- * agent left, or a turn declined in {@link BeforeAgentTurn}. Nothing new is written; the
2154
- * person's own row is only confirmed Complete.
2364
+ * Settles a message that starts no agent turn — `MentionOnly` with no tagged agent, a message
2365
+ * addressed to a person, no allowed agent left, or a turn declined in {@link BeforeAgentTurn}.
2366
+ * Nothing new is written; the person's own row is only confirmed Complete.
2155
2367
  */
2156
2368
  async finishWithoutAgentTurn(userMessage, reason) {
2157
2369
  // Under the default rules a turn always has somewhere to go, so reaching here with no agent
@@ -2257,18 +2469,30 @@ export class MessageInputComponent extends BaseAngularComponent {
2257
2469
  }
2258
2470
  await this.processMessageThroughAgent(messageDetail, mentionResult, branchId);
2259
2471
  }
2472
+ _cachedContinuityReplyKey;
2473
+ _cachedContinuityReply = null;
2260
2474
  /**
2261
2475
  * The newest reply from an agent other than the conversation manager — among the replies a
2262
- * turn may read, so from {@link AgentHistoryFrom} onward when it's set.
2476
+ * turn may read, so from {@link AgentHistoryFrom} onward when it's set. Resets to null when
2477
+ * a newer user message `@`-mentions the conversation manager or when the conversation manager
2478
+ * received a handback from another agent.
2263
2479
  */
2264
2480
  findLastNonSageReply() {
2265
- return this.ConversationHistory
2266
- .slice()
2267
- .reverse()
2268
- .find(msg => msg.Role === 'AI' &&
2269
- !!msg.AgentID &&
2270
- !UUIDsEqual(msg.AgentID, this.ConverationManagerAgent?.ID) &&
2271
- this.isWithinHistoryFloor(msg)) ?? null;
2481
+ const history = this.ConversationHistory ?? [];
2482
+ const lastRow = history.length > 0 ? history[history.length - 1] : null;
2483
+ const managerId = this.ConverationManagerAgent?.ID ?? null;
2484
+ const floorMs = this.AgentHistoryFrom ? new Date(this.AgentHistoryFrom).getTime() : '';
2485
+ const cacheKey = `${history.length}|${lastRow?.ID ?? ''}|${lastRow?.AgentID ?? ''}|${managerId ?? ''}|${floorMs}`;
2486
+ if (this._cachedContinuityReplyKey === cacheKey) {
2487
+ return this._cachedContinuityReply ?? null;
2488
+ }
2489
+ const reply = FindContinuityAgentMessage(history, managerId, {
2490
+ conversationManagerName: this.ConverationManagerAgent?.Name ?? 'Sage',
2491
+ isWithinFloor: msg => this.isWithinHistoryFloor(msg),
2492
+ });
2493
+ this._cachedContinuityReplyKey = cacheKey;
2494
+ this._cachedContinuityReply = reply;
2495
+ return reply;
2272
2496
  }
2273
2497
  /**
2274
2498
  * Finds the last agent ID that isn't Sage (see {@link findLastNonSageReply}).
@@ -2276,6 +2500,17 @@ export class MessageInputComponent extends BaseAngularComponent {
2276
2500
  findLastNonSageAgentId() {
2277
2501
  return this.findLastNonSageReply()?.AgentID || null;
2278
2502
  }
2503
+ /**
2504
+ * The newest reply from any agent (including Sage) within {@link AgentHistoryFrom} — used under
2505
+ * `MentionOnly`, where the conversation manager is not an implicit fallback and is only present
2506
+ * when explicitly tagged.
2507
+ */
2508
+ findLastReplyAgentId() {
2509
+ return this.ConversationHistory
2510
+ .slice()
2511
+ .reverse()
2512
+ .find(msg => msg.Role === 'AI' && !!msg.AgentID && this.isWithinHistoryFloor(msg))?.AgentID || null;
2513
+ }
2279
2514
  /** True when a row was written at or after {@link AgentHistoryFrom}; always true without one. */
2280
2515
  isWithinHistoryFloor(detail) {
2281
2516
  const floor = this.AgentHistoryFrom;
@@ -2289,14 +2524,20 @@ export class MessageInputComponent extends BaseAngularComponent {
2289
2524
  isKnownAgent(agentId) {
2290
2525
  return AIEngineBase.Instance.ReadableAgents.some(a => UUIDsEqual(a.ID, agentId));
2291
2526
  }
2527
+ /**
2528
+ * True when the current user can run the agent directly in this chat: it is in the '@' list
2529
+ * (`GetAvailableAgents`) or is an allowed restricted agent that passes the same active,
2530
+ * top-level, non-sub-agent, and run-permission checks.
2531
+ */
2532
+ isRunnableAgent(agentId) {
2533
+ const runnable = (this.mentionAutocomplete ?? MentionAutocompleteService.Instance).GetAvailableAgents();
2534
+ const findAgent = (id) => AIEngineBase.Instance.ReadableAgents.find(a => UUIDsEqual(a.ID, id));
2535
+ return this.findRoutingRunnableAgent(agentId, runnable, findAgent) != null;
2536
+ }
2292
2537
  /** The agent's name, when the client's agent catalog has it. */
2293
2538
  agentNameFor(agentId) {
2294
2539
  return AIEngineBase.Instance.ReadableAgents.find(a => UUIDsEqual(a.ID, agentId))?.Name ?? null;
2295
2540
  }
2296
- /** The ID of the agent with this name, or null when there is none. */
2297
- agentIdByName(agentName) {
2298
- return AIEngineBase.Instance.ReadableAgents.find(a => a.Name === agentName)?.ID ?? null;
2299
- }
2300
2541
  /** True when the agent is MJ's conversation manager. */
2301
2542
  isConversationManager(agentId) {
2302
2543
  return !!this.ConverationManagerAgent?.ID && UUIDsEqual(agentId, this.ConverationManagerAgent.ID);
@@ -2305,17 +2546,6 @@ export class MessageInputComponent extends BaseAngularComponent {
2305
2546
  notifyAgentTurnProblem(message, style) {
2306
2547
  MJNotificationService.Instance?.CreateSimpleNotification(message, style, 5000);
2307
2548
  }
2308
- /**
2309
- * Refuses agents the host doesn't allow — named by the conversation manager's delegation or
2310
- * by a workflow it planned — and settles the person's message.
2311
- */
2312
- async refuseDisallowedAgents(userMessage, agentNames) {
2313
- const subject = agentNames.join(', ');
2314
- this.notifyAgentTurnProblem(agentNames.length === 1
2315
- ? `${subject} isn't available in this chat, so it didn't run.`
2316
- : `${subject} aren't available in this chat, so they didn't run.`, 'warning');
2317
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2318
- }
2319
2549
  /**
2320
2550
  * Runs the turn through the host's {@link AgentTurnHandler} instead of MJ's path. Nothing is
2321
2551
  * written here: the host writes the reply rows and reports them, and each is shown like a row
@@ -2398,7 +2628,7 @@ export class MessageInputComponent extends BaseAngularComponent {
2398
2628
  // Form responses always continue with the agent that requested the form
2399
2629
  // Don't show "Analyzing intent..." message for this obvious case
2400
2630
  if (ConversationUtility.ContainsFormResponse(message)) {
2401
- console.log('✅ Form response detected, skipping intent check UI (fast path)');
2631
+ LogStatusEx({ message: '✅ Form response detected, skipping intent check UI (fast path)', verboseOnly: true });
2402
2632
  return {
2403
2633
  decision: 'YES',
2404
2634
  reasoning: 'User submitted a form response to the previous agent'
@@ -2530,221 +2760,76 @@ export class MessageInputComponent extends BaseAngularComponent {
2530
2760
  * @param branchId The turn's branch (null is Main).
2531
2761
  */
2532
2762
  async processMessageThroughAgent(userMessage, mentionResult, branchId) {
2533
- let taskId = null;
2534
2763
  let conversationManagerMessage = null;
2535
2764
  // CRITICAL: Capture conversationId from user message at start
2536
2765
  // This prevents race condition when user switches conversations during async processing
2537
2766
  const conversationId = userMessage.ConversationID;
2538
2767
  try {
2539
- // Create AI message for Sage BEFORE invoking
2540
- conversationManagerMessage = await this.createTurnRow(conversationId, branchId);
2541
- conversationManagerMessage.Message = '⏳ Starting...';
2542
- conversationManagerMessage.ParentID = userMessage.ID;
2543
- conversationManagerMessage.Status = 'In-Progress';
2544
- // Use the preloaded Sage agent instead of looking it up
2545
- if (this.ConverationManagerAgent?.ID) {
2546
- conversationManagerMessage.AgentID = this.ConverationManagerAgent.ID;
2547
- }
2548
- await conversationManagerMessage.Save();
2549
- this.MessageSent.emit(conversationManagerMessage);
2768
+ // Sage's reply shows as a local placeholder until the server creates the row and names it.
2769
+ conversationManagerMessage = await this.createReplyPlaceholder(conversationId, branchId, userMessage.ID, this.ConverationManagerAgent?.ID ?? null);
2550
2770
  // Use Sage to evaluate and route
2551
2771
  // Stage 1: Sage evaluates the message
2552
- taskId = this.activeTasks.add({
2772
+ const result = await this.agentService.ProcessMessage(conversationId, userMessage, this.ConversationHistory, this.adoptReplyRow(conversationManagerMessage, {
2553
2773
  agentName: 'Sage',
2554
2774
  status: 'Evaluating message...',
2555
2775
  relatedMessageId: userMessage.ID,
2556
- conversationDetailId: conversationManagerMessage.ID,
2557
2776
  conversationId,
2558
2777
  conversationName: this.ConversationName
2559
- });
2560
- const result = await this.agentService.ProcessMessage(conversationId, userMessage, this.ConversationHistory, conversationManagerMessage.ID, this.createProgressCallback(conversationManagerMessage, 'Sage'), this.AppContext, this.PlanModeEnabled, // per-request Plan Mode toggle
2778
+ }), this.createProgressCallback(conversationManagerMessage, 'Sage'), this.AppContext, this.PlanModeEnabled, // per-request Plan Mode toggle
2561
2779
  this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
2562
2780
  this.AllowedAgentIDs, // the agents the manager may delegate to
2563
- this.AgentHistoryFrom);
2781
+ this.AgentHistoryFrom, // the first moment of the conversation the run may read
2782
+ branchId);
2564
2783
  // Emit afterAgentTurn on the happy path only — the error/failure branch
2565
2784
  // immediately below handles its own cleanup and skips this emit.
2566
2785
  if (result && result.success) {
2567
2786
  this.emitAfterAgentTurn(conversationId, result);
2568
2787
  }
2569
- // Task will be removed automatically in markMessageComplete()
2570
- // DO NOT remove here - agent may still be streaming/processing
2571
- taskId = null; // Clear reference but don't remove from service
2572
2788
  if (!result || !result.success) {
2573
2789
  await this.applyAgentFailureToDetail(conversationManagerMessage, userMessage, this.ConverationManagerAgent?.Name || 'Sage', result, 'failed');
2574
2790
  console.warn('⚠️ Sage failed:', agentFailureMessage(result, 'Agent evaluation failed'));
2575
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2576
2791
  return;
2577
2792
  }
2578
- // Stage 2: Check for task graph (multi-step orchestration)
2579
- if (result.payload?.taskGraph) {
2580
- await this.handleTaskGraphExecution(userMessage, result, conversationId, branchId, conversationManagerMessage);
2581
- // Remove CM from active tasks
2582
- if (taskId) {
2583
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2584
- }
2585
- }
2586
- // Stage 3: Check for sub-agent invocation (single-step delegation)
2587
- else if (result.agentRun.FinalStep === 'Success' && result.payload?.invokeAgent) {
2588
- // Reuse the existing conversationManagerMessage instead of creating new ones
2589
- await this.handleSubAgentInvocation(userMessage, result, conversationId, branchId, conversationManagerMessage);
2590
- // Remove CM from active tasks
2591
- if (taskId) {
2592
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2593
- }
2594
- }
2595
- // Stage 4: Direct chat response from Agent
2596
- else if (result.agentRun.FinalStep === 'Chat' && result.agentRun.Message) {
2597
- // Normal chat response
2598
- // 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
2599
- // Note: updateConversationDetail will call markMessageComplete() for us
2600
- await this.updateConversationDetail(conversationManagerMessage, result.agentRun.Message, 'Complete', result);
2601
- // Handle artifacts if any (but NOT task graphs - those are intermediate work products)
2602
- // Server already created artifacts - just emit event to trigger UI reload
2603
- if (result.payload && Object.keys(result.payload).length > 0) {
2604
- this.emitArtifactReload(conversationManagerMessage);
2605
- this.MessageSent.emit(conversationManagerMessage);
2606
- }
2607
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2608
- // Remove CM from active tasks
2609
- if (taskId) {
2610
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2611
- }
2612
- // Clean up completion timestamp after delay
2613
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2614
- }
2615
- // Stage 5: Silent observation - but check for message content first
2616
- else {
2617
- // Check if there's a message to display even without payload/taskGraph
2618
- if (result.agentRun.Message) {
2619
- // Mark message as completing BEFORE setting final content
2620
- this.markMessageComplete(conversationManagerMessage);
2621
- conversationManagerMessage.HiddenToUser = false;
2622
- // 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
2623
- await this.updateConversationDetail(conversationManagerMessage, result.agentRun.Message, 'Complete', result);
2624
- this.MessageSent.emit(conversationManagerMessage);
2625
- // Clean up completion timestamp after delay
2626
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2627
- }
2628
- else {
2629
- // Mark message as completing
2630
- this.markMessageComplete(conversationManagerMessage);
2631
- // Hide the Sage message
2632
- conversationManagerMessage.HiddenToUser = true;
2633
- // 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
2634
- await this.updateConversationDetail(conversationManagerMessage, conversationManagerMessage.Message, 'Complete', result);
2635
- this.MessageSent.emit(conversationManagerMessage);
2636
- await this.handleSilentObservation(userMessage, conversationId, branchId);
2637
- // Clean up completion timestamp after delay
2638
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2639
- }
2640
- // Remove CM from active tasks
2641
- if (taskId) {
2642
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2643
- }
2644
- }
2793
+ // The server wrote Sage's reply; show it as the server left it before routing on.
2794
+ await this.settleReply(conversationManagerMessage, '❌ Sage could not start.');
2795
+ await this.routeConversationManagerResult(userMessage, result, conversationId, branchId, conversationManagerMessage);
2645
2796
  }
2646
2797
  catch (error) {
2647
2798
  console.error('❌ Error processing message through agents:', error);
2648
- // Update conversationManagerMessage status to Error
2649
- if (conversationManagerMessage && conversationManagerMessage.ID) {
2650
- // Use updateConversationDetail to ensure task cleanup
2651
- conversationManagerMessage.Error = String(error);
2652
- await this.updateConversationDetail(conversationManagerMessage, `❌ Error: ${String(error)}`, 'Error');
2653
- // Clean up completion timestamp
2654
- this.cleanupCompletionTimestamp(conversationManagerMessage.ID);
2799
+ if (conversationManagerMessage) {
2800
+ await this.settleReply(conversationManagerMessage, `❌ Error: ${String(error)}`);
2655
2801
  }
2656
2802
  // Mark user message as complete
2657
2803
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2658
- // Clean up active task
2659
- if (taskId) {
2660
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2661
- }
2662
- }
2663
- }
2664
- /** Detaches the progress callback for one message — used when submission never starts. */
2665
- releaseProgressCallback(messageId) {
2666
- const callback = this.registeredCallbacks.get(messageId);
2667
- if (callback) {
2668
- this.streamingService.unregisterMessageCallback(messageId, callback);
2669
- this.registeredCallbacks.delete(messageId);
2670
2804
  }
2671
2805
  }
2672
2806
  /**
2673
- * Submits a task graph to the server and returns — the client no longer drives execution.
2674
- *
2675
- * This used to call the `ExecuteTaskGraph` mutation and await the ENTIRE workflow inside one
2676
- * long-lived GraphQL request. That had three unfixable problems: a page reload lost the awaited
2677
- * promise (leaving a workflow running with nobody watching), a server restart orphaned every
2678
- * in-flight task, and no other channel could reach the substrate at all.
2679
- *
2680
- * Now submission returns as soon as the graph is durable and the server-side dispatcher executes
2681
- * it. The client is purely an observer: progress arrives over the existing PubSub frames, and
2682
- * because the work lives in Task rows rather than in a promise, a reload can re-attach to a
2683
- * workflow already in flight.
2684
- *
2685
- * Single-task graphs are no longer special-cased here. The old client-side fork ran them through
2686
- * a different code path entirely; they now submit like any other graph, and the decision about
2687
- * whether a one-node graph is worth durable machinery moves server-side where it can be recorded.
2688
- *
2689
- * @param branchId The turn's branch (null is Main); the task-graph message is written there.
2807
+ * Acts on the conversation manager's settled reply: a chat answer or silence (its reply is
2808
+ * hidden and the last agent may continue). Handoffs and task graphs execute server-side.
2690
2809
  */
2691
- async handleTaskGraphExecution(userMessage, managerResult, conversationId, branchId, conversationManagerMessage) {
2692
- // `payload` is untyped by construction (an agent's payload shape is agent-specific), so pin the
2693
- // graph to the operation's own input contract at the boundary rather than letting it stay loose.
2694
- const taskGraph = managerResult.payload?.taskGraph;
2695
- if (!taskGraph)
2696
- return;
2697
- // The host's allowed list holds for every agent step of the plan, not only for a single
2698
- // delegation: a workflow naming an agent the chat doesn't allow is not submitted at all.
2699
- const disallowedAgents = FindDisallowedTaskGraphAgents(Array.isArray(taskGraph.tasks) ? taskGraph.tasks : [], this.AllowedAgentIDs, agentName => this.agentIdByName(agentName));
2700
- if (disallowedAgents.length > 0) {
2701
- await this.refuseDisallowedAgents(userMessage, disallowedAgents);
2702
- return;
2703
- }
2704
- const workflowName = taskGraph.workflowName || 'Workflow';
2705
- const reasoning = taskGraph.reasoning || 'Executing multi-step workflow';
2706
- const taskCount = Array.isArray(taskGraph.tasks) ? taskGraph.tasks.length : 0;
2707
- // A message the user can watch. Progress frames from the dispatcher land against this ID.
2708
- const taskExecutionMessage = await this.createTurnRow(conversationId, branchId);
2709
- taskExecutionMessage.Message = `⏳ **${workflowName}**\n\n${reasoning}\n\nSubmitting ${taskCount} task(s)…`;
2710
- taskExecutionMessage.ParentID = conversationManagerMessage.ID;
2711
- taskExecutionMessage.Status = 'In-Progress';
2712
- await taskExecutionMessage.Save();
2713
- this.MessageSent.emit(taskExecutionMessage);
2714
- const callback = this.createMessageProgressCallback(taskExecutionMessage.ID);
2715
- this.registeredCallbacks.set(taskExecutionMessage.ID, callback);
2716
- this.streamingService.registerMessageCallback(taskExecutionMessage.ID, callback);
2717
- try {
2718
- // `TaskGraph.Submit` is a Remote Operation, not a bespoke mutation: the same call site is
2719
- // reachable from MCP, an Action wrapper, and this UI. `Execute` marshals over the generic
2720
- // `ExecuteRemoteOperation` transport, so there is no hand-written GraphQL document here.
2721
- const result = await new TaskGraphSubmitOperation().Execute({
2722
- spec: taskGraph,
2723
- environmentID: MJEnvironmentEntityExtended.DefaultEnvironmentID,
2724
- conversationDetailID: taskExecutionMessage.ID,
2725
- });
2726
- if (result.Success && result.Output?.success) {
2727
- // Deliberately NOT "completed" — submission means the work is durable and running, and
2728
- // claiming completion here is exactly the lie the old await-everything path told when it
2729
- // returned early. The dispatcher's progress frames update this message as tasks finish.
2730
- await this.updateConversationDetail(taskExecutionMessage, `▶️ **${workflowName}** started — ${taskCount} task(s) running.`, 'In-Progress');
2731
- }
2732
- else {
2733
- const errorMsg = result.Output?.errorMessage || result.ErrorMessage || 'Unknown error';
2734
- console.error('Task graph submission rejected:', errorMsg);
2735
- taskExecutionMessage.Error = errorMsg;
2736
- await this.updateConversationDetail(taskExecutionMessage, `❌ **${workflowName}** rejected: ${errorMsg}`, 'Error');
2737
- this.releaseProgressCallback(taskExecutionMessage.ID);
2810
+ async routeConversationManagerResult(userMessage, result, conversationId, branchId, managerReply) {
2811
+ if (result.agentRun.FinalStep === 'Chat' && result.agentRun.Message) {
2812
+ // Handle artifacts if any (but NOT task graphs - those are intermediate work products)
2813
+ // Server already created artifacts - just emit event to trigger UI reload
2814
+ if (result.payload && Object.keys(result.payload).length > 0) {
2815
+ this.emitArtifactReload(managerReply);
2816
+ this.MessageSent.emit(managerReply);
2738
2817
  }
2818
+ await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2739
2819
  }
2740
- catch (error) {
2741
- const msg = error instanceof Error ? error.message : String(error);
2742
- console.error('Task graph submission failed:', error);
2743
- taskExecutionMessage.Error = msg;
2744
- await this.updateConversationDetail(taskExecutionMessage, `❌ **${workflowName}** failed to submit: ${msg}`, 'Error');
2745
- this.releaseProgressCallback(taskExecutionMessage.ID);
2820
+ else if (result.agentRun.Message) {
2821
+ await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2822
+ }
2823
+ else {
2824
+ await this.hideReply(managerReply);
2825
+ await this.handleSilentObservation(userMessage, conversationId, branchId);
2746
2826
  }
2747
2827
  }
2828
+ /**
2829
+ * Saves the person's own message with its text and status (in MJ's chat, marking it Complete once its turn is
2830
+ * over). Never used for an agent's reply: only the server writes those, and the chat shows them with
2831
+ * {@link settleReply}.
2832
+ */
2748
2833
  async updateConversationDetail(convoDetail, message, status, result) {
2749
2834
  // Mark as completing FIRST if status is Complete or Error
2750
2835
  // This ensures task cleanup happens even if we return early due to guard clause
@@ -2815,10 +2900,10 @@ export class MessageInputComponent extends BaseAngularComponent {
2815
2900
  }
2816
2901
  const source = await this.agentService.FindLatestAgentOutputVersion(this.ConversationId, agentId, this.AgentHistoryFrom, branchId);
2817
2902
  if (!source || source.payload == null) {
2818
- console.log(`📦 No previous payload found for agent ${agentId}`);
2903
+ LogStatusEx({ message: `📦 No previous payload found for agent ${agentId}`, verboseOnly: true });
2819
2904
  return { payload: null, artifactInfo: null };
2820
2905
  }
2821
- console.log(`📦 Loaded previous payload for agent ${agentId} from artifact version ${source.versionId}`);
2906
+ LogStatusEx({ message: `📦 Loaded previous payload for agent ${agentId} from artifact version ${source.versionId}`, verboseOnly: true });
2822
2907
  return {
2823
2908
  payload: source.payload,
2824
2909
  artifactInfo: {
@@ -2829,139 +2914,15 @@ export class MessageInputComponent extends BaseAngularComponent {
2829
2914
  };
2830
2915
  }
2831
2916
  /**
2832
- * Handle sub-agent invocation based on Sage's payload
2833
- * Reuses the existing conversationManagerMessage to avoid creating multiple records
2834
- *
2835
- * @param branchId The turn's branch (null is Main).
2917
+ * Shows a delegate's successful reply as the server wrote it, reloads its artifacts (the server
2918
+ * may have created some even when the result payload is empty, e.g. a remote stage server), and
2919
+ * completes the user's message.
2836
2920
  */
2837
- async handleSubAgentInvocation(userMessage, managerResult, conversationId, branchId, conversationManagerMessage) {
2838
- const payload = managerResult.payload;
2839
- const agentName = payload.invokeAgent;
2840
- const reasoning = payload.reasoning || 'Delegating to specialist agent';
2841
- // Now create a NEW message for the sub-agent execution
2842
- let agentResponseMessage = null;
2843
- try {
2844
- // Look up the agent to get its ID
2845
- const agent = AIEngineBase.Instance.ReadableAgents.find(a => a.Name === agentName);
2846
- // The manager routes only among the allowed agents, but its answer is model output: hold
2847
- // the host's list here too, before any row is written for the delegate.
2848
- if (this.AllowedAgentIDs != null && !IsAgentAllowed(agent?.ID, this.AllowedAgentIDs)) {
2849
- await this.refuseDisallowedAgents(userMessage, [agentName]);
2850
- return;
2851
- }
2852
- // Create AI response message BEFORE invoking agent (for duration tracking)
2853
- agentResponseMessage = await this.createTurnRow(conversationId, branchId);
2854
- agentResponseMessage.Message = '⏳ Starting...'; // Initial message
2855
- agentResponseMessage.ParentID = conversationManagerMessage.ID; // Thread under delegation message
2856
- agentResponseMessage.Status = 'In-Progress';
2857
- // Set AgentID immediately for proper attribution
2858
- if (agent?.ID) {
2859
- agentResponseMessage.AgentID = agent.ID;
2860
- }
2861
- // Save the record to establish __mj_CreatedAt timestamp
2862
- await agentResponseMessage.Save();
2863
- this.MessageSent.emit(agentResponseMessage);
2864
- // Add sub-agent to active tasks
2865
- const newTaskId = this.activeTasks.add({
2866
- agentName: agentName,
2867
- status: 'Starting...',
2868
- relatedMessageId: userMessage.ID,
2869
- conversationDetailId: agentResponseMessage.ID,
2870
- conversationId,
2871
- conversationName: this.ConversationName
2872
- });
2873
- // Load previous payload if agent has been invoked before
2874
- const { payload: previousPayload, artifactInfo } = agent?.ID
2875
- ? await this.loadPreviousPayloadForAgent(agent.ID, branchId)
2876
- : { payload: null, artifactInfo: null };
2877
- // Find configuration preset from previous @mention in conversation history
2878
- const configurationPresetId = agent?.ID
2879
- ? await this.agentService.FindConfigurationPresetForAgent(conversationId, agent.ID, branchId)
2880
- : undefined;
2881
- // Invoke the sub-agent with progress callback
2882
- const subResult = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, reasoning, agentResponseMessage.ID, previousPayload, // Pass previous payload for continuity
2883
- this.createProgressCallback(agentResponseMessage, agentName), artifactInfo?.artifactId, artifactInfo?.versionId, configurationPresetId, // Pass configuration from previous @mention for continuity
2884
- this.AppContext, // Embedder-supplied app/form context
2885
- this.PlanModeEnabled, // per-request Plan Mode toggle
2886
- this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
2887
- this.AgentHistoryFrom);
2888
- // Task will be removed automatically in markMessageComplete() when status changes to Complete/Error
2889
- // DO NOT remove here - allows UI to show task during entire execution
2890
- if (subResult && subResult.success) {
2891
- // Update the response message with agent result
2892
- // Store the agent ID for display
2893
- if (subResult.agentRun.AgentID) {
2894
- agentResponseMessage.AgentID = subResult.agentRun.AgentID;
2895
- }
2896
- await this.updateConversationDetail(agentResponseMessage, subResult.agentRun?.Message || `✅ **${agentName}** completed`, 'Complete', subResult);
2897
- // Always emit artifactCreated to trigger UI reload — the server may have created
2898
- // artifacts even when the result payload is empty (e.g., remote stage server).
2899
- // onArtifactCreated will reload from DB and discover any artifacts that exist.
2900
- this.emitArtifactReload(agentResponseMessage);
2901
- this.MessageSent.emit(agentResponseMessage);
2902
- // Mark user message as complete
2903
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2904
- }
2905
- else {
2906
- // A post-ACK disconnect means the first run may still be executing on this
2907
- // detail — do not start a second run on the same conversationDetailId.
2908
- if (agentFailureDisposition(subResult).status === 'In-Progress') {
2909
- await this.applyAgentFailureToDetail(agentResponseMessage, userMessage, agentName, subResult);
2910
- return;
2911
- }
2912
- // Sub-agent failed - attempt auto-retry once
2913
- console.log(`⚠️ ${agentName} failed, attempting auto-retry...`);
2914
- await this.updateConversationDetail(conversationManagerMessage, `👉 **${agentName}** will handle this request...\n\n⚠️ First attempt failed, retrying...`, conversationManagerMessage.Status);
2915
- // Update the existing agentResponseMessage to show retry status
2916
- await this.updateConversationDetail(agentResponseMessage, "Retrying...", agentResponseMessage.Status);
2917
- // Retry the sub-agent (reuse previously loaded payload and config from first attempt)
2918
- const retryResult = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, reasoning, agentResponseMessage.ID, previousPayload, // Pass same payload as first attempt
2919
- this.createProgressCallback(agentResponseMessage, `${agentName} (retry)`), artifactInfo?.artifactId, artifactInfo?.versionId, configurationPresetId, // Pass same config as first attempt
2920
- this.AppContext, // Embedder-supplied app/form context
2921
- this.PlanModeEnabled, // per-request Plan Mode toggle
2922
- this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
2923
- this.AgentHistoryFrom);
2924
- if (retryResult && retryResult.success) {
2925
- // Retry succeeded - update the same message
2926
- if (retryResult.agentRun.AgentID) {
2927
- agentResponseMessage.AgentID = retryResult.agentRun.AgentID;
2928
- }
2929
- await this.updateConversationDetail(agentResponseMessage, retryResult.agentRun?.Message || `✅ **${agentName}** completed`, 'Complete', retryResult);
2930
- // Always emit artifactCreated to trigger UI reload (same as initial attempt)
2931
- this.emitArtifactReload(agentResponseMessage);
2932
- this.MessageSent.emit(agentResponseMessage);
2933
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2934
- }
2935
- else {
2936
- // Retry also failed — terminate the agent bubble (the red-pill timer lives here),
2937
- // not only the Sage delegation message.
2938
- await this.applyAgentFailureToDetail(agentResponseMessage, userMessage, agentName, retryResult, 'failed after retry');
2939
- const retryDisposition = agentFailureDisposition(retryResult);
2940
- if (retryDisposition.status === 'Error') {
2941
- conversationManagerMessage.Error = retryDisposition.message;
2942
- await this.updateConversationDetail(conversationManagerMessage, `❌ **${agentName}** failed after retry\n\n${retryDisposition.message}`, 'Error');
2943
- }
2944
- }
2945
- }
2946
- }
2947
- catch (error) {
2948
- console.error(`❌ Error invoking sub-agent ${agentName}:`, error);
2949
- const catchResult = {
2950
- success: false,
2951
- errorMessage: error instanceof Error ? error.message : String(error),
2952
- };
2953
- if (agentResponseMessage) {
2954
- await this.applyAgentFailureToDetail(agentResponseMessage, userMessage, agentName, catchResult);
2955
- }
2956
- else {
2957
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2958
- }
2959
- const catchDisposition = agentFailureDisposition(catchResult);
2960
- if (catchDisposition.status === 'Error') {
2961
- conversationManagerMessage.Error = catchDisposition.message;
2962
- await this.updateConversationDetail(conversationManagerMessage, `❌ **${agentName}** encountered an error\n\n${catchDisposition.message}`, 'Error');
2963
- }
2964
- }
2921
+ async showDelegateReply(reply, userMessage) {
2922
+ await this.settleReply(reply, '❌ The agent could not start.');
2923
+ this.emitArtifactReload(reply);
2924
+ this.MessageSent.emit(reply);
2925
+ await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2965
2926
  }
2966
2927
  /**
2967
2928
  * Handle silent observation - when Sage stays silent,
@@ -2974,12 +2935,12 @@ export class MessageInputComponent extends BaseAngularComponent {
2974
2935
  const lastAIMessage = this.findLastNonSageReply();
2975
2936
  if (!lastAIMessage || !lastAIMessage.AgentID) {
2976
2937
  // No previous specialist agent - just mark user message as complete
2977
- console.log('🔇 No previous specialist agent found - marking complete');
2938
+ LogStatusEx({ message: '🔇 No previous specialist agent found - marking complete', verboseOnly: true });
2978
2939
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2979
2940
  return;
2980
2941
  }
2981
2942
  if (!IsAgentAllowed(lastAIMessage.AgentID, this.AllowedAgentIDs)) {
2982
- console.log('🔇 Previous specialist agent is not allowed in this chat - marking complete');
2943
+ LogStatusEx({ message: '🔇 Previous specialist agent is not allowed in this chat - marking complete', verboseOnly: true });
2983
2944
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2984
2945
  return;
2985
2946
  }
@@ -3004,60 +2965,39 @@ export class MessageInputComponent extends BaseAngularComponent {
3004
2965
  versionId: source.versionId,
3005
2966
  versionNumber: source.versionNumber
3006
2967
  };
3007
- console.log('📦 Loaded previous OUTPUT artifact as payload for continuity', previousArtifactInfo);
3008
- }
3009
- // Create status message showing agent continuity
3010
- const statusMessage = await this.createTurnRow(conversationId, branchId);
3011
- statusMessage.Message = `Continuing with **${agentName}** for refinement...`;
3012
- statusMessage.ParentID = userMessage.ID;
3013
- statusMessage.Status = 'Complete';
3014
- statusMessage.AgentID = this.ConverationManagerAgent?.ID || null;
3015
- await statusMessage.Save();
3016
- this.MessageSent.emit(statusMessage);
3017
- // Add agent to active tasks
3018
- const taskId = this.activeTasks.add({
3019
- agentName: agentName,
3020
- status: 'Processing refinement...',
3021
- relatedMessageId: userMessage.ID,
3022
- conversationDetailId: statusMessage.ID,
3023
- conversationId,
3024
- conversationName: this.ConversationName
3025
- });
2968
+ LogStatusEx({ message: '📦 Loaded previous OUTPUT artifact as payload for continuity', verboseOnly: true, additionalArgs: [previousArtifactInfo] });
2969
+ }
2970
+ // The manager's line "Continuing with X for refinement..." and the continuing agent's reply, threaded under it,
2971
+ // both written by the server. Until it acknowledges the run they show as local placeholders.
2972
+ const managerId = this.ConverationManagerAgent?.ID ?? null;
2973
+ const announcement = managerId
2974
+ ? await this.createReplyPlaceholder(conversationId, branchId, userMessage.ID, managerId, { Message: ContinuationAnnouncementText(agentName), Status: 'Complete' })
2975
+ : undefined;
2976
+ const reply = await this.createReplyPlaceholder(conversationId, branchId, announcement?.ID ?? userMessage.ID, lastAIMessage.AgentID);
2977
+ const threading = managerId || branchId
2978
+ ? {
2979
+ ...(managerId ? { AnnouncedByAgentId: managerId } : {}),
2980
+ ...(branchId ? { ReplyBranchId: branchId } : {}),
2981
+ }
2982
+ : null;
3026
2983
  try {
3027
2984
  // Invoke the agent with the previous payload
3028
- const continuityResult = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, 'Continuing previous work based on user feedback', statusMessage.ID, previousPayload, this.createProgressCallback(statusMessage, agentName), previousArtifactInfo?.artifactId, previousArtifactInfo?.versionId, undefined, // configurationPresetId not used in this path
2985
+ const continuityResult = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, 'Continuing previous work based on user feedback', this.adoptReplyRow(reply, { agentName, status: 'Processing refinement...', relatedMessageId: userMessage.ID, conversationId, conversationName: this.ConversationName }, announcement), previousPayload, this.createProgressCallback(reply, agentName), previousArtifactInfo?.artifactId, previousArtifactInfo?.versionId, undefined, // configurationPresetId not used in this path
3029
2986
  this.AppContext, // Embedder-supplied app/form context
3030
2987
  this.PlanModeEnabled, // per-request Plan Mode toggle
3031
2988
  this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
3032
- this.AgentHistoryFrom);
3033
- // Remove from active tasks
3034
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
2989
+ this.AgentHistoryFrom, // the first moment of the conversation the run may read
2990
+ threading);
3035
2991
  if (continuityResult && continuityResult.success) {
3036
- // Create response message
3037
- const agentResponseMessage = await this.createTurnRow(conversationId, branchId);
3038
- agentResponseMessage.Message = continuityResult.agentRun?.Message || `✅ **${agentName}** completed refinement`;
3039
- agentResponseMessage.ParentID = statusMessage.ID;
3040
- agentResponseMessage.Status = 'Complete';
3041
- agentResponseMessage.AgentID = lastAIMessage.AgentID;
3042
- await agentResponseMessage.Save();
3043
- this.MessageSent.emit(agentResponseMessage);
3044
- // Server created artifacts (handles versioning automatically) - emit event to trigger UI reload
3045
- if (continuityResult.payload && Object.keys(continuityResult.payload).length > 0) {
3046
- this.emitArtifactReload(agentResponseMessage);
3047
- console.log('🎨 Server created artifact (versioned) from agent continuity');
3048
- this.MessageSent.emit(agentResponseMessage);
3049
- }
3050
- // Mark user message as complete
3051
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
2992
+ await this.showDelegateReply(reply, userMessage);
3052
2993
  }
3053
2994
  else {
3054
- await this.applyAgentFailureToDetail(statusMessage, userMessage, agentName, continuityResult, 'failed during refinement');
2995
+ await this.applyAgentFailureToDetail(reply, userMessage, agentName, continuityResult, 'failed during refinement');
3055
2996
  }
3056
2997
  }
3057
2998
  catch (error) {
3058
2999
  console.error(`❌ Error in agent continuity with ${agentName}:`, error);
3059
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
3060
- await this.applyAgentFailureToDetail(statusMessage, userMessage, agentName, { success: false, errorMessage: error instanceof Error ? error.message : String(error) }, 'encountered an error');
3000
+ await this.applyAgentFailureToDetail(reply, userMessage, agentName, { success: false, errorMessage: error instanceof Error ? error.message : String(error) }, 'encountered an error');
3061
3001
  }
3062
3002
  }
3063
3003
  /**
@@ -3068,15 +3008,6 @@ export class MessageInputComponent extends BaseAngularComponent {
3068
3008
  */
3069
3009
  async invokeAgentDirectly(userMessage, agentMention, conversationId, branchId) {
3070
3010
  const agentName = agentMention.name;
3071
- // Add agent to active tasks
3072
- const taskId = this.activeTasks.add({
3073
- agentName: agentName,
3074
- status: 'Processing...',
3075
- relatedMessageId: userMessage.ID,
3076
- conversationDetailId: userMessage.ID,
3077
- conversationId,
3078
- conversationName: this.ConversationName
3079
- });
3080
3011
  // Declare agentResponseMessage outside try block so it's accessible in catch
3081
3012
  let agentResponseMessage = undefined;
3082
3013
  try {
@@ -3087,58 +3018,31 @@ export class MessageInputComponent extends BaseAngularComponent {
3087
3018
  this.MessageSent.emit(userMessage);
3088
3019
  // Look up the agent to get its ID
3089
3020
  const agent = AIEngineBase.Instance.ReadableAgents.find(a => a.Name === agentName);
3090
- // Create AI response message BEFORE invoking agent (for duration tracking)
3091
- agentResponseMessage = await this.createTurnRow(conversationId, branchId);
3092
- agentResponseMessage.Message = '⏳ Starting...'; // Initial message
3093
- agentResponseMessage.ParentID = userMessage.ID;
3094
- agentResponseMessage.Status = 'In-Progress';
3095
- // Set AgentID immediately for proper attribution
3096
- if (agent?.ID) {
3097
- agentResponseMessage.AgentID = agent.ID;
3098
- }
3099
- // Save the record to establish __mj_CreatedAt timestamp
3100
- await agentResponseMessage.Save();
3101
- this.MessageSent.emit(agentResponseMessage);
3021
+ // The reply shows as a local placeholder until the server creates the row and names it.
3022
+ agentResponseMessage = await this.createReplyPlaceholder(conversationId, branchId, userMessage.ID, agent?.ID ?? null);
3102
3023
  // Load previous payload if agent has been invoked before
3103
3024
  const { payload: previousPayload, artifactInfo } = agent?.ID
3104
3025
  ? await this.loadPreviousPayloadForAgent(agent.ID, branchId)
3105
3026
  : { payload: null, artifactInfo: null };
3106
3027
  // Invoke the agent directly
3107
- const result = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, `User mentioned agent directly with @${agentName}`, agentResponseMessage.ID, previousPayload, // Pass previous payload for continuity
3028
+ const result = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, `User mentioned agent directly with @${agentName}`, this.adoptReplyRow(agentResponseMessage, { agentName, status: 'Processing...', relatedMessageId: userMessage.ID, conversationId, conversationName: this.ConversationName }), previousPayload, // Pass previous payload for continuity
3108
3029
  this.createProgressCallback(agentResponseMessage, agentName), artifactInfo?.artifactId, artifactInfo?.versionId, agentMention.configurationId, // Pass configuration preset ID
3109
3030
  this.AppContext, // Embedder-supplied app/form context
3110
3031
  this.PlanModeEnabled, // per-request Plan Mode toggle
3111
3032
  this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
3112
- this.AgentHistoryFrom);
3113
- // Remove from active tasks
3114
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
3033
+ this.AgentHistoryFrom, // the first moment of the conversation the run may read
3034
+ branchId ? { ReplyBranchId: branchId } : null);
3115
3035
  if (result && result.success) {
3116
- if (result.agentRun.AgentID) {
3117
- agentResponseMessage.AgentID = result.agentRun.AgentID;
3118
- }
3119
3036
  this.emitAfterAgentTurn(conversationId, result);
3120
- // Multi-stage response handling (same logic as ambient Sage)
3121
- // Stage 1: Check for task graph (multi-step orchestration)
3122
- if (result.payload?.taskGraph) {
3123
- console.log('📋 Task graph detected from @mention, starting task orchestration');
3124
- await this.handleTaskGraphExecution(userMessage, result, conversationId, branchId, agentResponseMessage);
3125
- }
3126
- // Stage 2: Check for sub-agent invocation (single-step delegation)
3127
- else if (result.agentRun.FinalStep === 'Success' && result.payload?.invokeAgent) {
3128
- console.log('🎯 Sub-agent invocation detected from @mention');
3129
- await this.handleSubAgentInvocation(userMessage, result, conversationId, branchId, agentResponseMessage);
3130
- }
3131
- // Stage 3: Normal chat response
3132
- else {
3133
- await this.updateConversationDetail(agentResponseMessage, result.agentRun?.Message || `✅ **${agentName}** completed`, 'Complete', result);
3134
- // Server created artifacts - emit event to trigger UI reload
3135
- if (result.payload && Object.keys(result.payload).length > 0) {
3136
- this.emitArtifactReload(agentResponseMessage);
3137
- this.MessageSent.emit(agentResponseMessage);
3138
- }
3139
- // Mark user message as complete
3140
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
3037
+ await this.settleReply(agentResponseMessage, `❌ **${agentName}** could not start.`);
3038
+ // Normal chat response (handoffs and task graphs now execute server-side in AgentRunner)
3039
+ // Server created artifacts - emit event to trigger UI reload
3040
+ if (result.payload && Object.keys(result.payload).length > 0) {
3041
+ this.emitArtifactReload(agentResponseMessage);
3042
+ this.MessageSent.emit(agentResponseMessage);
3141
3043
  }
3044
+ // Mark user message as complete
3045
+ await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
3142
3046
  }
3143
3047
  else {
3144
3048
  await this.applyAgentFailureToDetail(agentResponseMessage, userMessage, agentName, result);
@@ -3154,6 +3058,40 @@ export class MessageInputComponent extends BaseAngularComponent {
3154
3058
  }
3155
3059
  }
3156
3060
  }
3061
+ /**
3062
+ * Resolves the configuration preset for an implicit continuation turn on `agentId` using
3063
+ * `ResolveConversationAgentPreset`. Uses `this.AgentConfigurationPresetId` only when that
3064
+ * preset belongs to `agentId` (`preset.AgentID`), else falls back to `agentId`'s resolved preset.
3065
+ */
3066
+ async resolveContinuationPresetForAgent(conversationId, agentId, branchId) {
3067
+ const resolved = await ResolveConversationAgentPreset({
3068
+ ConversationID: conversationId,
3069
+ AgentID: agentId,
3070
+ BranchID: branchId,
3071
+ ContextUser: this.CurrentUser,
3072
+ Provider: this.ProviderToUse,
3073
+ });
3074
+ if (resolved?.Preset?.ID && resolved.Source !== 'Default') {
3075
+ return resolved.Preset.ID;
3076
+ }
3077
+ const headerPresetId = this.headerPresetForAgent(agentId);
3078
+ if (headerPresetId) {
3079
+ return headerPresetId;
3080
+ }
3081
+ return resolved?.Preset?.ID ?? undefined;
3082
+ }
3083
+ /**
3084
+ * Returns `this.AgentConfigurationPresetId` only when it belongs to `agentId` (`preset.AgentID === agentId`).
3085
+ */
3086
+ headerPresetForAgent(agentId) {
3087
+ const candidateId = this.AgentConfigurationPresetId;
3088
+ if (!candidateId) {
3089
+ return undefined;
3090
+ }
3091
+ const allForAgent = AIEngineBase.Instance.GetAgentConfigurationPresets(agentId, false);
3092
+ const matched = (allForAgent ?? []).find(p => UUIDsEqual(p.ID, candidateId) && (!p.AgentID || UUIDsEqual(p.AgentID, agentId)));
3093
+ return matched?.ID ?? undefined;
3094
+ }
3157
3095
  /**
3158
3096
  * Continue with the same agent from previous message (implicit continuation)
3159
3097
  * Bypasses Sage - no status messages
@@ -3177,7 +3115,6 @@ export class MessageInputComponent extends BaseAngularComponent {
3177
3115
  const agentName = agent.Name || 'Agent';
3178
3116
  let previousPayload = null;
3179
3117
  let previousArtifactInfo = null;
3180
- let previousConfigurationId = undefined;
3181
3118
  // Use targetArtifactVersionId if specified (from intent check).
3182
3119
  // Resolved by ID against the database, not against artifactsByDetailId: the intent check
3183
3120
  // may well have named a version attached to a message BELOW the loaded window, which is
@@ -3191,24 +3128,18 @@ export class MessageInputComponent extends BaseAngularComponent {
3191
3128
  versionId: target.versionId,
3192
3129
  versionNumber: target.versionNumber
3193
3130
  };
3194
- console.log('📦 Loaded target artifact version as payload', previousArtifactInfo);
3131
+ LogStatusEx({ message: '📦 Loaded target artifact version as payload', verboseOnly: true, additionalArgs: [previousArtifactInfo] });
3195
3132
  }
3196
3133
  else {
3197
3134
  console.warn('⚠️ Could not load target artifact version:', targetArtifactVersionId);
3198
3135
  }
3199
3136
  }
3200
- // Extract configuration preset from the User message that @mentioned this agent
3201
- // Uses the shared helper method in the agent service
3202
- previousConfigurationId = await this.agentService.FindConfigurationPresetForAgent(conversationId, agentId, branchId);
3203
- // Fall back to the chat header's mode-picker selection when nothing
3204
- // in the message history pinned a preset. The picker reflects the
3205
- // user's persistent per-agent mode preference (Draft / Standard /
3206
- // High) and applies to all subsequent non-mention routes. A
3207
- // history-derived preset still wins because it represents an
3208
- // explicit per-message intent the user expressed earlier.
3209
- if (!previousConfigurationId && this.AgentConfigurationPresetId) {
3210
- previousConfigurationId = this.AgentConfigurationPresetId;
3211
- }
3137
+ // Resolve a preset for the continued agent via ResolveConversationAgentPreset (which checks
3138
+ // @mention pins first, then user settings, then the agent's default preset).
3139
+ // The header mode-picker selection (this.AgentConfigurationPresetId) is used only when it
3140
+ // belongs to this agent (preset.AgentID === agentId); otherwise the agent's own default
3141
+ // preset is resolved so a pre-handoff picker selection from another agent never leaks.
3142
+ const previousConfigurationId = await this.resolveContinuationPresetForAgent(conversationId, agentId, branchId);
3212
3143
  // Fall back to searching through all agent messages for an artifact
3213
3144
  // This ensures payload continuity even after clarifying exchanges without artifacts
3214
3145
  // Fall back to this agent's newest OUTPUT artifact when the intent check named no
@@ -3224,10 +3155,10 @@ export class MessageInputComponent extends BaseAngularComponent {
3224
3155
  versionId: source.versionId,
3225
3156
  versionNumber: source.versionNumber
3226
3157
  };
3227
- console.log('📦 Loaded artifact as payload for continuation', previousArtifactInfo);
3158
+ LogStatusEx({ message: '📦 Loaded artifact as payload for continuation', verboseOnly: true, additionalArgs: [previousArtifactInfo] });
3228
3159
  }
3229
3160
  else {
3230
- console.log(`📦 No artifact found for agent ${agentId} in this conversation`);
3161
+ LogStatusEx({ message: `📦 No artifact found for agent ${agentId} in this conversation`, verboseOnly: true });
3231
3162
  }
3232
3163
  }
3233
3164
  // Execute the agent with the gathered context
@@ -3247,15 +3178,6 @@ export class MessageInputComponent extends BaseAngularComponent {
3247
3178
  * @param configurationId Optional configuration preset ID to use
3248
3179
  */
3249
3180
  async executeAgentContinuation(userMessage, agentId, agentName, conversationId, branchId, previousPayload, previousArtifactInfo, configurationId) {
3250
- // Add agent to active tasks
3251
- const taskId = this.activeTasks.add({
3252
- agentName: agentName,
3253
- status: 'Processing...',
3254
- relatedMessageId: userMessage.ID,
3255
- conversationDetailId: userMessage.ID,
3256
- conversationId,
3257
- conversationName: this.ConversationName
3258
- });
3259
3181
  // Declare agentResponseMessage outside try block so it's accessible in catch
3260
3182
  let agentResponseMessage = undefined;
3261
3183
  try {
@@ -3264,32 +3186,23 @@ export class MessageInputComponent extends BaseAngularComponent {
3264
3186
  userMessage.Status = 'Complete';
3265
3187
  await userMessage.Save();
3266
3188
  this.MessageSent.emit(userMessage);
3267
- // Create AI response message BEFORE invoking agent (for duration tracking)
3268
- agentResponseMessage = await this.createTurnRow(conversationId, branchId);
3269
- agentResponseMessage.Message = '⏳ Starting...'; // Initial message
3270
- agentResponseMessage.ParentID = userMessage.ID;
3271
- agentResponseMessage.Status = 'In-Progress';
3272
- agentResponseMessage.AgentID = agentId;
3273
- // Save the record to establish __mj_CreatedAt timestamp
3274
- await agentResponseMessage.Save();
3275
- this.MessageSent.emit(agentResponseMessage);
3189
+ // The reply shows as a local placeholder until the server creates the row and names it.
3190
+ agentResponseMessage = await this.createReplyPlaceholder(conversationId, branchId, userMessage.ID, agentId);
3276
3191
  // Invoke the agent directly (continuation) with previous payload if available.
3277
3192
  // `this.appContext` is forwarded so direct-routed sub-agents (e.g. Form
3278
3193
  // Builder via [defaultAgentId]) see the embedder's ActiveForm/Schema/
3279
3194
  // OverrideID block in their prompt — same flow Sage gets via
3280
3195
  // `processMessage`.
3281
- const result = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, 'Continuing previous conversation with user', agentResponseMessage.ID, previousPayload, // Pass previous OUTPUT artifact payload for continuity
3196
+ const result = await this.agentService.invokeSubAgent(agentName, conversationId, userMessage, this.ConversationHistory, 'Continuing previous conversation with user', this.adoptReplyRow(agentResponseMessage, { agentName, status: 'Processing...', relatedMessageId: userMessage.ID, conversationId, conversationName: this.ConversationName }), previousPayload, // Pass previous OUTPUT artifact payload for continuity
3282
3197
  this.createProgressCallback(agentResponseMessage, agentName), previousArtifactInfo?.artifactId, previousArtifactInfo?.versionId, configurationId, // Pass configuration for continuity
3283
3198
  this.AppContext, // Embedder-supplied app/form context
3284
3199
  this.PlanModeEnabled, // per-request Plan Mode toggle
3285
3200
  this._pendingRequestedSkillIDs, // user-requested skills (/skill mentions)
3286
- this.AgentHistoryFrom);
3287
- // Remove from active tasks
3288
- // Task removed in markMessageComplete() - this.activeTasks.remove(taskId);
3201
+ this.AgentHistoryFrom, // the first moment of the conversation the run may read
3202
+ branchId ? { ReplyBranchId: branchId } : null);
3289
3203
  if (result && result.success) {
3290
3204
  this.emitAfterAgentTurn(conversationId, result);
3291
- // Update the response message with agent result
3292
- await this.updateConversationDetail(agentResponseMessage, result.agentRun?.Message || `✅ **${agentName}** completed`, 'Complete', result);
3205
+ await this.settleReply(agentResponseMessage, `❌ **${agentName}** could not start.`);
3293
3206
  // Server created artifacts (handles versioning) - emit event to trigger UI reload
3294
3207
  if (result.payload && Object.keys(result.payload).length > 0) {
3295
3208
  this.emitArtifactReload(agentResponseMessage);
@@ -3336,39 +3249,128 @@ export class MessageInputComponent extends BaseAngularComponent {
3336
3249
  name: result.Name,
3337
3250
  description: result.Description
3338
3251
  });
3339
- console.log(`✅ Conversation renamed to: "${result.Name}"`);
3252
+ LogStatusEx({ message: `✅ Conversation renamed to: "${result.Name}"`, verboseOnly: true });
3340
3253
  }
3341
3254
  }
3342
3255
  /**
3343
- * Persist an agent failure onto the response bubble.
3256
+ * An unsaved, local "Starting…" reply, shown from the moment the turn starts until the server
3257
+ * acknowledges the run and names the reply row it created. The chat never writes an agent's
3258
+ * reply: only the server does (A19). The placeholder is never saved; {@link adoptReplyRow}
3259
+ * turns it into the server's row.
3260
+ */
3261
+ async createReplyPlaceholder(conversationId, branchId, parentId, agentId, line = { Message: '⏳ Starting...', Status: 'In-Progress' }) {
3262
+ const placeholder = await this.dataCache.createConversationDetail(this.CurrentUser);
3263
+ placeholder.ConversationID = conversationId;
3264
+ placeholder.BranchID = branchId; // the server writes the reply on its message's branch; the placeholder shows there too
3265
+ placeholder.Role = 'AI';
3266
+ placeholder.Message = line.Message;
3267
+ placeholder.ParentID = parentId;
3268
+ placeholder.Status = line.Status;
3269
+ placeholder.HiddenToUser = false;
3270
+ placeholder.AgentID = agentId;
3271
+ this.MessageSent.emit(placeholder);
3272
+ return placeholder;
3273
+ }
3274
+ /**
3275
+ * The acknowledgement callback for a placeholder's run. When the server accepts the run it names
3276
+ * the reply row it created: the placeholder takes that row's ID and creation time (so the bubble's
3277
+ * timer starts when the server's row did), and the turn's active task is keyed by that ID, which
3278
+ * is what its progress and completion carry. Runs synchronously, before any progress is routed.
3279
+ */
3280
+ adoptReplyRow(placeholder, task, announcement) {
3281
+ return (reply) => {
3282
+ if (announcement && reply.AnnouncementDetailId) {
3283
+ this.adoptRow(announcement, reply.AnnouncementDetailId, reply.CreatedAt);
3284
+ }
3285
+ this.adoptRow(placeholder, reply.ConversationDetailId, reply.CreatedAt);
3286
+ this.activeTasks.add({ ...task, conversationDetailId: reply.ConversationDetailId });
3287
+ };
3288
+ }
3289
+ /** Makes a local placeholder the server's row: its ID and creation time. Synchronous, so nothing routes in between. */
3290
+ adoptRow(placeholder, id, createdAt) {
3291
+ void placeholder.LoadFromData({ ...placeholder.GetAll(), ID: id, __mj_CreatedAt: createdAt }, true);
3292
+ this.MessageSent.emit(placeholder);
3293
+ }
3294
+ /**
3295
+ * Shows a reply as the server left it once its run has returned. The server wrote the reply's
3296
+ * text and status (the chat no longer saves it), so the row is reloaded. A row still In-Progress
3297
+ * is watched until the server settles it. A placeholder the server never acknowledged has no row:
3298
+ * it is marked failed on screen only, with `failureText`, and never saved.
3299
+ */
3300
+ async settleReply(reply, failureText) {
3301
+ if (!reply.IsSaved) {
3302
+ this.markMessageComplete(reply);
3303
+ reply.Status = 'Error';
3304
+ reply.Message = failureText;
3305
+ this.MessageSent.emit(reply);
3306
+ this.cleanupCompletionTimestamp(reply.ID);
3307
+ return;
3308
+ }
3309
+ await reply.Load(reply.ID);
3310
+ if (reply.Status === 'In-Progress') {
3311
+ this.MessageSent.emit(reply);
3312
+ this.startInFlightDetailWatch(reply);
3313
+ return;
3314
+ }
3315
+ this.markMessageComplete(reply);
3316
+ this.MessageSent.emit(reply);
3317
+ this.cleanupCompletionTimestamp(reply.ID);
3318
+ }
3319
+ /**
3320
+ * Hides a reply from the transcript (bookkeeping a person may still write; what the reply says
3321
+ * stays the server's). Used for a manager's silent turn and for a delegate's failed first attempt
3322
+ * once its retry takes over.
3323
+ */
3324
+ async hideReply(reply) {
3325
+ reply.HiddenToUser = true;
3326
+ if (reply.IsSaved && !(await reply.Save())) {
3327
+ console.warn(`Could not hide reply ${reply.ID}:`, reply.LatestResult?.CompleteMessage);
3328
+ }
3329
+ this.MessageSent.emit(reply);
3330
+ }
3331
+ /**
3332
+ * Shows an agent failure on the reply bubble.
3344
3333
  *
3345
- * A dropped HTTP/WebSocket path used to return `null` from invokeSubAgent, so
3346
- * the bubble said "Unknown error" while the AIAgentRun stayed Running and the
3347
- * timer kept ticking. If the transport ACKed the mutation and then died, keep
3348
- * In-Progress so a later completion event (or {@link startInFlightDetailWatch})
3349
- * can land. ConversationDetail.Status is the server's claim; the client only
3350
- * renders it. The GraphQLAIClient stall reconciler covers the wait inside
3351
- * invokeSubAgent; once that returns, the watch observes the detail until the
3352
- * server writes a terminal status (MaxTimePerRun).
3334
+ * The server writes the reply, failures included (the chat never saves it), so this reloads the
3335
+ * row the server wrote. A dropped HTTP/WebSocket path after the server ACKed the run may leave the
3336
+ * run still executing: the bubble then says so, on screen only, and {@link startInFlightDetailWatch}
3337
+ * follows the row until the server writes a terminal status. A placeholder the server never
3338
+ * acknowledged shows the failure on screen only.
3353
3339
  *
3354
- * Always completes the user message — the user turn finished regardless of
3355
- * what the agent is doing.
3340
+ * A run in AwaitingFeedback/Paused is not a failure at all — the agent asked this user a
3341
+ * question, or is parked on a workflow. It only reaches here because the envelope says
3342
+ * `success: false`, and because such a run usually carries no ErrorMessage it used to render as
3343
+ * "❌ failed — Unknown error": a verdict the server never returned. That branch surfaces the
3344
+ * agent's question and completes the bubble instead.
3345
+ *
3346
+ * Always completes the user message — the user turn finished regardless of what the agent is doing.
3356
3347
  */
3357
3348
  async applyAgentFailureToDetail(agentResponseMessage, userMessage, agentName, result, failedVerb = 'failed') {
3358
3349
  const disposition = agentFailureDisposition(result);
3359
- if (disposition.status === 'In-Progress') {
3360
- await this.updateConversationDetail(agentResponseMessage, `⏳ **${agentName}** is still running on the server.\n\n${disposition.message}`, 'In-Progress');
3361
- // Skip the watch if the reload-before-write guard already found a
3362
- // terminal server status (Complete/Error) — starting it would race the
3363
- // just-completed bubble.
3350
+ if (disposition.status === 'Awaiting-Input') {
3351
+ // Complete, not Error: the turn is over (the timer must stop) and the agent's question is
3352
+ // the reply. `result` is passed through so any ResponseForm the agent attached to its
3353
+ // question still renders. The user's answer resumes the run, which re-enters
3354
+ // AgentStateService's active set as Running.
3355
+ await this.updateConversationDetail(agentResponseMessage, disposition.message || `**${agentName}** needs more information to continue — please reply with details.`, 'Complete', result ?? undefined);
3356
+ await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
3357
+ return;
3358
+ }
3359
+ if (disposition.status === 'In-Progress' && agentResponseMessage.IsSaved) {
3360
+ await agentResponseMessage.Load(agentResponseMessage.ID);
3364
3361
  if (agentResponseMessage.Status === 'In-Progress') {
3362
+ agentResponseMessage.Message = `⏳ **${agentName}** is still running on the server.\n\n${disposition.message}`;
3363
+ this.MessageSent.emit(agentResponseMessage);
3365
3364
  this.startInFlightDetailWatch(agentResponseMessage);
3366
3365
  }
3367
- await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
3368
- return;
3366
+ else {
3367
+ this.markMessageComplete(agentResponseMessage);
3368
+ this.MessageSent.emit(agentResponseMessage);
3369
+ }
3370
+ }
3371
+ else {
3372
+ await this.settleReply(agentResponseMessage, `❌ **${agentName}** ${failedVerb}\n\n${disposition.message}`);
3369
3373
  }
3370
- agentResponseMessage.Error = disposition.message;
3371
- await this.updateConversationDetail(agentResponseMessage, `❌ **${agentName}** ${failedVerb}\n\n${disposition.message}`, 'Error');
3372
3374
  await this.updateConversationDetail(userMessage, userMessage.Message, 'Complete');
3373
3375
  }
3374
3376
  startInFlightDetailWatch(detail) {
@@ -3525,28 +3527,31 @@ export class MessageInputComponent extends BaseAngularComponent {
3525
3527
  } if (rf & 2) {
3526
3528
  let _t;
3527
3529
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.InputBox = _t.first);
3528
- } }, inputs: { ConversationId: "ConversationId", conversationId: "conversationId", ConversationName: "ConversationName", conversationName: "conversationName", CurrentUser: "CurrentUser", currentUser: "currentUser", Disabled: "Disabled", disabled: "disabled", ReadOnly: "ReadOnly", SendSlotMode: "SendSlotMode", StopCount: "StopCount", StopLabel: "StopLabel", StatusText: "StatusText", StatusElapsed: "StatusElapsed", Placeholder: "Placeholder", placeholder: "placeholder", ParentMessageId: "ParentMessageId", parentMessageId: "parentMessageId", EnableAttachments: "EnableAttachments", enableAttachments: "enableAttachments", EnableMentions: "EnableMentions", enableMentions: "enableMentions", EnableAgentMentions: "EnableAgentMentions", enableAgentMentions: "enableAgentMentions", EnableEntityMentions: "EnableEntityMentions", enableEntityMentions: "enableEntityMentions", EnableSkillCommands: "EnableSkillCommands", enableSkillCommands: "enableSkillCommands", EnablePlanMode: "EnablePlanMode", enablePlanMode: "enablePlanMode", EnableRealtime: "EnableRealtime", enableRealtime: "enableRealtime", MaxAttachments: "MaxAttachments", maxAttachments: "maxAttachments", MaxAttachmentSizeBytes: "MaxAttachmentSizeBytes", maxAttachmentSizeBytes: "maxAttachmentSizeBytes", AcceptedFileTypes: "AcceptedFileTypes", acceptedFileTypes: "acceptedFileTypes", ArtifactsByDetailId: "ArtifactsByDetailId", artifactsByDetailId: "artifactsByDetailId", SystemArtifactsByDetailId: "SystemArtifactsByDetailId", systemArtifactsByDetailId: "systemArtifactsByDetailId", AgentRunsByDetailId: "AgentRunsByDetailId", agentRunsByDetailId: "agentRunsByDetailId", EmptyStateMode: "EmptyStateMode", emptyStateMode: "emptyStateMode", AppContext: "AppContext", appContext: "appContext", DefaultAgentId: "DefaultAgentId", defaultAgentId: "defaultAgentId", ConversationDefaultAgentId: "ConversationDefaultAgentId", conversationDefaultAgentId: "conversationDefaultAgentId", AgentConfigurationPresetId: "AgentConfigurationPresetId", agentConfigurationPresetId: "agentConfigurationPresetId", InitialMessage: "InitialMessage", initialMessage: "initialMessage", InitialAttachments: "InitialAttachments", initialAttachments: "initialAttachments", ConversationHistory: "ConversationHistory", conversationHistory: "conversationHistory", InProgressMessageIds: "InProgressMessageIds", inProgressMessageIds: "inProgressMessageIds", ApplicationId: "ApplicationId", applicationId: "applicationId", AgentReplyMode: "AgentReplyMode", AllowedAgentIDs: "AllowedAgentIDs", MentionPeople: "MentionPeople", AgentHistoryFrom: "AgentHistoryFrom", TargetBranchID: "TargetBranchID", ResolveTargetBranch: "ResolveTargetBranch", AgentTurnHandler: "AgentTurnHandler", AutoNameConversation: "AutoNameConversation", EnableDecisionRouting: "EnableDecisionRouting", InitialDraft: "InitialDraft", initialDraft: "initialDraft" }, outputs: { StopClicked: "StopClicked", MessageSent: "MessageSent", messageSent: "messageSent", MessageStreamed: "MessageStreamed", AgentResponse: "AgentResponse", agentResponse: "agentResponse", BeforeAgentTurn: "BeforeAgentTurn", beforeAgentTurn: "beforeAgentTurn", AfterAgentTurn: "AfterAgentTurn", afterAgentTurn: "afterAgentTurn", AgentRunDetected: "AgentRunDetected", agentRunDetected: "agentRunDetected", AgentRunUpdate: "AgentRunUpdate", agentRunUpdate: "agentRunUpdate", MessageComplete: "MessageComplete", messageComplete: "messageComplete", ArtifactCreated: "ArtifactCreated", artifactCreated: "artifactCreated", ConversationRenamed: "ConversationRenamed", conversationRenamed: "conversationRenamed", IntentCheckStarted: "IntentCheckStarted", intentCheckStarted: "intentCheckStarted", IntentCheckCompleted: "IntentCheckCompleted", intentCheckCompleted: "intentCheckCompleted", InitialMessageAutoSendStarted: "InitialMessageAutoSendStarted", initialMessageAutoSendStarted: "initialMessageAutoSendStarted", InitialMessageAutoSendFailed: "InitialMessageAutoSendFailed", initialMessageAutoSendFailed: "initialMessageAutoSendFailed", EmptyStateSubmit: "EmptyStateSubmit", emptyStateSubmit: "emptyStateSubmit", UploadStateChanged: "UploadStateChanged", uploadStateChanged: "uploadStateChanged", InitialDraftApplied: "InitialDraftApplied", initialDraftApplied: "initialDraftApplied", DraftStateChanged: "DraftStateChanged", ComposerBlurred: "ComposerBlurred" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature, i0.ɵɵNgOnChangesFeature], decls: 6, vars: 34, consts: [["pickerOrigin", "cdkOverlayOrigin"], ["inputBox", ""], ["cdkOverlayOrigin", "", 1, "message-input-wrapper"], [1, "processing-indicator"], [3, "StopClicked", "valueChange", "blurred", "textSubmitted", "attachmentsChanged", "attachmentError", "voiceRequested", "voiceOptionsRequested", "planModeToggle", "placeholder", "disabled", "showCharacterCount", "enableMentions", "EnableAgentMentions", "EnableEntityMentions", "EnableSkillCommands", "TargetAgentId", "AllowedAgentIDs", "MentionPeople", "enableAttachments", "maxAttachments", "maxAttachmentSizeBytes", "acceptedFileTypes", "currentUser", "Provider", "rows", "enableRealtime", "voiceActive", "canStartRealtime", "enablePlanMode", "planModeActive", "SendSlotMode", "StopCount", "StopLabel", "StatusText", "StatusElapsed", "value"], ["cdkConnectedOverlay", "", "cdkConnectedOverlayBackdropClass", "cdk-overlay-transparent-backdrop", 3, "backdropClick", "detach", "cdkConnectedOverlayOrigin", "cdkConnectedOverlayOpen", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPush", "cdkConnectedOverlayHasBackdrop"], [1, "fas", "fa-circle-notch", "fa-spin"], [3, "AgentPicked", "Cancelled", "Provider", "CurrentUser", "Agents", "DefaultAgentId", "CoAgents", "DefaultCoAgentId"]], template: function MessageInputComponent_Template(rf, ctx) { if (rf & 1) {
3530
+ } }, inputs: { ConversationId: "ConversationId", conversationId: "conversationId", ConversationName: "ConversationName", conversationName: "conversationName", CurrentUser: "CurrentUser", currentUser: "currentUser", Disabled: "Disabled", disabled: "disabled", ReadOnly: "ReadOnly", SendSlotMode: "SendSlotMode", StopCount: "StopCount", StopLabel: "StopLabel", StatusText: "StatusText", StatusElapsed: "StatusElapsed", Placeholder: "Placeholder", placeholder: "placeholder", ParentMessageId: "ParentMessageId", parentMessageId: "parentMessageId", EnableAttachments: "EnableAttachments", enableAttachments: "enableAttachments", EnableMentions: "EnableMentions", enableMentions: "enableMentions", EnableAgentMentions: "EnableAgentMentions", enableAgentMentions: "enableAgentMentions", EnableEntityMentions: "EnableEntityMentions", enableEntityMentions: "enableEntityMentions", EnableSkillCommands: "EnableSkillCommands", enableSkillCommands: "enableSkillCommands", EnablePlanMode: "EnablePlanMode", enablePlanMode: "enablePlanMode", EnableRealtime: "EnableRealtime", enableRealtime: "enableRealtime", MaxAttachments: "MaxAttachments", maxAttachments: "maxAttachments", MaxAttachmentSizeBytes: "MaxAttachmentSizeBytes", maxAttachmentSizeBytes: "maxAttachmentSizeBytes", AcceptedFileTypes: "AcceptedFileTypes", acceptedFileTypes: "acceptedFileTypes", ArtifactsByDetailId: "ArtifactsByDetailId", artifactsByDetailId: "artifactsByDetailId", SystemArtifactsByDetailId: "SystemArtifactsByDetailId", systemArtifactsByDetailId: "systemArtifactsByDetailId", AgentRunsByDetailId: "AgentRunsByDetailId", agentRunsByDetailId: "agentRunsByDetailId", EmptyStateMode: "EmptyStateMode", emptyStateMode: "emptyStateMode", AppContext: "AppContext", appContext: "appContext", DefaultAgentId: "DefaultAgentId", defaultAgentId: "defaultAgentId", ConversationDefaultAgentId: "ConversationDefaultAgentId", conversationDefaultAgentId: "conversationDefaultAgentId", AgentConfigurationPresetId: "AgentConfigurationPresetId", agentConfigurationPresetId: "agentConfigurationPresetId", InitialMessage: "InitialMessage", initialMessage: "initialMessage", InitialAttachments: "InitialAttachments", initialAttachments: "initialAttachments", ConversationHistory: "ConversationHistory", conversationHistory: "conversationHistory", InProgressMessageIds: "InProgressMessageIds", inProgressMessageIds: "inProgressMessageIds", ApplicationId: "ApplicationId", applicationId: "applicationId", AgentReplyMode: "AgentReplyMode", AllowedAgentIDs: "AllowedAgentIDs", MentionPeople: "MentionPeople", AgentHistoryFrom: "AgentHistoryFrom", TargetBranchID: "TargetBranchID", ResolveTargetBranch: "ResolveTargetBranch", AgentTurnHandler: "AgentTurnHandler", AutoNameConversation: "AutoNameConversation", EnableDecisionRouting: "EnableDecisionRouting", InitialDraft: "InitialDraft", initialDraft: "initialDraft" }, outputs: { StopClicked: "StopClicked", MessageSent: "MessageSent", messageSent: "messageSent", MessageStreamed: "MessageStreamed", AgentResponse: "AgentResponse", agentResponse: "agentResponse", BeforeAgentTurn: "BeforeAgentTurn", beforeAgentTurn: "beforeAgentTurn", AfterAgentTurn: "AfterAgentTurn", afterAgentTurn: "afterAgentTurn", AgentRunDetected: "AgentRunDetected", agentRunDetected: "agentRunDetected", AgentRunUpdate: "AgentRunUpdate", agentRunUpdate: "agentRunUpdate", MessageComplete: "MessageComplete", messageComplete: "messageComplete", ArtifactCreated: "ArtifactCreated", artifactCreated: "artifactCreated", ConversationRenamed: "ConversationRenamed", conversationRenamed: "conversationRenamed", IntentCheckStarted: "IntentCheckStarted", intentCheckStarted: "intentCheckStarted", IntentCheckCompleted: "IntentCheckCompleted", intentCheckCompleted: "intentCheckCompleted", InitialMessageAutoSendStarted: "InitialMessageAutoSendStarted", initialMessageAutoSendStarted: "initialMessageAutoSendStarted", InitialMessageAutoSendFailed: "InitialMessageAutoSendFailed", initialMessageAutoSendFailed: "initialMessageAutoSendFailed", EmptyStateSubmit: "EmptyStateSubmit", emptyStateSubmit: "emptyStateSubmit", UploadStateChanged: "UploadStateChanged", uploadStateChanged: "uploadStateChanged", InitialDraftApplied: "InitialDraftApplied", initialDraftApplied: "initialDraftApplied", DraftStateChanged: "DraftStateChanged", ComposerBlurred: "ComposerBlurred" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature, i0.ɵɵNgOnChangesFeature], decls: 7, vars: 35, consts: [["pickerOrigin", "cdkOverlayOrigin"], ["inputBox", ""], ["cdkOverlayOrigin", "", 1, "message-input-wrapper"], [1, "processing-indicator"], [1, "composer-route", "route", 3, "--agent-accent"], [3, "StopClicked", "valueChange", "blurred", "textSubmitted", "attachmentsChanged", "attachmentError", "voiceRequested", "voiceOptionsRequested", "planModeToggle", "placeholder", "disabled", "showCharacterCount", "enableMentions", "EnableAgentMentions", "EnableEntityMentions", "EnableSkillCommands", "TargetAgentId", "AllowedAgentIDs", "MentionPeople", "enableAttachments", "maxAttachments", "maxAttachmentSizeBytes", "acceptedFileTypes", "currentUser", "Provider", "rows", "enableRealtime", "voiceActive", "canStartRealtime", "enablePlanMode", "planModeActive", "SendSlotMode", "StopCount", "StopLabel", "StatusText", "StatusElapsed", "value"], ["cdkConnectedOverlay", "", "cdkConnectedOverlayBackdropClass", "cdk-overlay-transparent-backdrop", 3, "backdropClick", "detach", "cdkConnectedOverlayOrigin", "cdkConnectedOverlayOpen", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPush", "cdkConnectedOverlayHasBackdrop"], [1, "fas", "fa-circle-notch", "fa-spin"], [1, "composer-route", "route"], [1, "to"], ["aria-hidden", "true", 1, "dot"], ["type", "button", 1, "linkish"], ["type", "button", 1, "linkish", 3, "click"], [3, "AgentPicked", "Cancelled", "Provider", "CurrentUser", "Agents", "DefaultAgentId", "CoAgents", "DefaultCoAgentId"]], template: function MessageInputComponent_Template(rf, ctx) { if (rf & 1) {
3529
3531
  i0.ɵɵelementStart(0, "div", 2, 0);
3530
3532
  i0.ɵɵconditionalCreate(2, MessageInputComponent_Conditional_2_Template, 4, 1, "div", 3);
3531
- i0.ɵɵelementStart(3, "mj-ai-composer", 4, 1);
3532
- i0.ɵɵlistener("StopClicked", function MessageInputComponent_Template_mj_ai_composer_StopClicked_3_listener() { return ctx.StopClicked.emit(); })("valueChange", function MessageInputComponent_Template_mj_ai_composer_valueChange_3_listener($event) { return ctx.OnComposerValueChanged($event); })("blurred", function MessageInputComponent_Template_mj_ai_composer_blurred_3_listener() { return ctx.ComposerBlurred.emit(); })("textSubmitted", function MessageInputComponent_Template_mj_ai_composer_textSubmitted_3_listener($event) { return ctx.onTextSubmitted($event); })("attachmentsChanged", function MessageInputComponent_Template_mj_ai_composer_attachmentsChanged_3_listener($event) { return ctx.onAttachmentsChanged($event); })("attachmentError", function MessageInputComponent_Template_mj_ai_composer_attachmentError_3_listener($event) { return ctx.onAttachmentError($event); })("voiceRequested", function MessageInputComponent_Template_mj_ai_composer_voiceRequested_3_listener() { return ctx.onStartRealtime(); })("voiceOptionsRequested", function MessageInputComponent_Template_mj_ai_composer_voiceOptionsRequested_3_listener() { return ctx.onRealtimeOptions(); })("planModeToggle", function MessageInputComponent_Template_mj_ai_composer_planModeToggle_3_listener() { return ctx.TogglePlanMode(); });
3533
+ i0.ɵɵconditionalCreate(3, MessageInputComponent_Conditional_3_Template, 8, 4, "div", 4);
3534
+ i0.ɵɵelementStart(4, "mj-ai-composer", 5, 1);
3535
+ i0.ɵɵlistener("StopClicked", function MessageInputComponent_Template_mj_ai_composer_StopClicked_4_listener() { return ctx.StopClicked.emit(); })("valueChange", function MessageInputComponent_Template_mj_ai_composer_valueChange_4_listener($event) { return ctx.OnComposerValueChanged($event); })("blurred", function MessageInputComponent_Template_mj_ai_composer_blurred_4_listener() { return ctx.ComposerBlurred.emit(); })("textSubmitted", function MessageInputComponent_Template_mj_ai_composer_textSubmitted_4_listener($event) { return ctx.onTextSubmitted($event); })("attachmentsChanged", function MessageInputComponent_Template_mj_ai_composer_attachmentsChanged_4_listener($event) { return ctx.onAttachmentsChanged($event); })("attachmentError", function MessageInputComponent_Template_mj_ai_composer_attachmentError_4_listener($event) { return ctx.onAttachmentError($event); })("voiceRequested", function MessageInputComponent_Template_mj_ai_composer_voiceRequested_4_listener() { return ctx.onStartRealtime(); })("voiceOptionsRequested", function MessageInputComponent_Template_mj_ai_composer_voiceOptionsRequested_4_listener() { return ctx.onRealtimeOptions(); })("planModeToggle", function MessageInputComponent_Template_mj_ai_composer_planModeToggle_4_listener() { return ctx.TogglePlanMode(); });
3533
3536
  i0.ɵɵelementEnd();
3534
- i0.ɵɵtemplate(5, MessageInputComponent_ng_template_5_Template, 1, 6, "ng-template", 5);
3535
- i0.ɵɵlistener("backdropClick", function MessageInputComponent_Template_ng_template_backdropClick_5_listener() { return ctx.onRealtimeAgentPickerCancelled(); })("detach", function MessageInputComponent_Template_ng_template_detach_5_listener() { return ctx.onRealtimeAgentPickerCancelled(); });
3537
+ i0.ɵɵtemplate(6, MessageInputComponent_ng_template_6_Template, 1, 6, "ng-template", 6);
3538
+ i0.ɵɵlistener("backdropClick", function MessageInputComponent_Template_ng_template_backdropClick_6_listener() { return ctx.onRealtimeAgentPickerCancelled(); })("detach", function MessageInputComponent_Template_ng_template_detach_6_listener() { return ctx.onRealtimeAgentPickerCancelled(); });
3536
3539
  i0.ɵɵelementEnd();
3537
3540
  } if (rf & 2) {
3538
- const pickerOrigin_r3 = i0.ɵɵreference(1);
3541
+ const pickerOrigin_r4 = i0.ɵɵreference(1);
3539
3542
  i0.ɵɵadvance(2);
3540
3543
  i0.ɵɵconditional(ctx.isProcessing ? 2 : -1);
3541
3544
  i0.ɵɵadvance();
3545
+ i0.ɵɵconditional(!ctx.emptyStateMode && ctx.CurrentRouteAgent ? 3 : -1);
3546
+ i0.ɵɵadvance();
3542
3547
  i0.ɵɵproperty("placeholder", ctx.placeholder)("disabled", ctx.disabled || ctx.isProcessing)("showCharacterCount", false)("enableMentions", ctx.enableMentions)("EnableAgentMentions", ctx.enableAgentMentions)("EnableEntityMentions", ctx.enableEntityMentions)("EnableSkillCommands", ctx.enableSkillCommands)("TargetAgentId", ctx.pickerTargetAgentId)("AllowedAgentIDs", ctx.AllowedAgentIDs)("MentionPeople", ctx.MentionPeople)("enableAttachments", ctx.enableAttachments)("maxAttachments", ctx.maxAttachments)("maxAttachmentSizeBytes", ctx.maxAttachmentSizeBytes)("acceptedFileTypes", ctx.acceptedFileTypes)("currentUser", ctx.currentUser)("Provider", ctx.Provider)("rows", 3)("enableRealtime", ctx.enableRealtime)("voiceActive", ctx.voiceActive)("canStartRealtime", ctx.canStartRealtime)("enablePlanMode", ctx.enablePlanMode)("planModeActive", ctx.PlanModeEnabled)("SendSlotMode", ctx.SendSlotMode)("StopCount", ctx.StopCount)("StopLabel", ctx.StopLabel)("StatusText", ctx.StatusText)("StatusElapsed", ctx.StatusElapsed)("value", ctx.messageText);
3543
3548
  i0.ɵɵadvance(2);
3544
- i0.ɵɵproperty("cdkConnectedOverlayOrigin", pickerOrigin_r3)("cdkConnectedOverlayOpen", ctx.showRealtimeAgentPicker)("cdkConnectedOverlayPositions", ctx.pickerOverlayPositions)("cdkConnectedOverlayPush", true)("cdkConnectedOverlayHasBackdrop", true);
3545
- } }, dependencies: [i11.CdkConnectedOverlay, i11.CdkOverlayOrigin, i12.RealtimeAgentPickerComponent, i13.AiComposerComponent], styles: [".message-input-wrapper[_ngcontent-%COMP%] {\n position: relative;\n width: 100%;\n}\n\n\n\n\n.processing-indicator[_ngcontent-%COMP%] {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1.25rem;\n background: color-mix(in srgb, var(--mj-bg-surface-card) 95%, transparent);\n border-radius: 8px;\n box-shadow: var(--mj-shadow-sm);\n z-index: 10;\n pointer-events: none;\n}\n.processing-indicator[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n.processing-indicator[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\n font-size: 0.9rem;\n color: var(--mj-text-primary);\n}"] });
3549
+ i0.ɵɵproperty("cdkConnectedOverlayOrigin", pickerOrigin_r4)("cdkConnectedOverlayOpen", ctx.showRealtimeAgentPicker)("cdkConnectedOverlayPositions", ctx.pickerOverlayPositions)("cdkConnectedOverlayPush", true)("cdkConnectedOverlayHasBackdrop", true);
3550
+ } }, dependencies: [i11.CdkConnectedOverlay, i11.CdkOverlayOrigin, i12.RealtimeAgentPickerComponent, i13.AiComposerComponent], styles: [".message-input-wrapper[_ngcontent-%COMP%] {\n position: relative;\n width: 100%;\n}\n\n\n\n\n.processing-indicator[_ngcontent-%COMP%] {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1.25rem;\n background: color-mix(in srgb, var(--mj-bg-surface-card) 95%, transparent);\n border-radius: 8px;\n box-shadow: var(--mj-shadow-sm);\n z-index: 10;\n pointer-events: none;\n}\n.processing-indicator[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n.processing-indicator[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\n font-size: 0.9rem;\n color: var(--mj-text-primary);\n}\n\n.composer-route[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 10px 6px;\n font-size: 11.5px;\n color: var(--mj-text-muted);\n}\n\n.composer-route[_ngcontent-%COMP%] .to[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n\n.composer-route[_ngcontent-%COMP%] .to[_ngcontent-%COMP%] b[_ngcontent-%COMP%] {\n font-weight: 600;\n color: var(--mj-text-secondary);\n}\n\n.composer-route[_ngcontent-%COMP%] .dot[_ngcontent-%COMP%] {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--agent-accent, var(--mj-brand-primary));\n flex-shrink: 0;\n transition: background-color 240ms cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.composer-route[_ngcontent-%COMP%] .linkish[_ngcontent-%COMP%] {\n margin-left: auto;\n padding: 2px 4px;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: var(--mj-brand-primary);\n font-family: inherit;\n font-size: 11.5px;\n font-weight: 500;\n text-decoration: underline;\n text-decoration-style: dotted;\n text-decoration-thickness: 1px;\n text-underline-offset: 2px;\n cursor: pointer;\n}\n\n.composer-route[_ngcontent-%COMP%] .linkish[_ngcontent-%COMP%]:hover {\n color: var(--mj-brand-primary-hover);\n}\n\n.composer-route[_ngcontent-%COMP%] .linkish[_ngcontent-%COMP%]:focus-visible {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n box-shadow: var(--mj-focus-ring, none);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .composer-route[_ngcontent-%COMP%] .dot[_ngcontent-%COMP%] {\n transition: none;\n }\n}"] });
3546
3551
  }
3547
3552
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MessageInputComponent, [{
3548
3553
  type: Component,
3549
- args: [{ standalone: false, selector: 'mj-message-input', template: "<div class=\"message-input-wrapper\" cdkOverlayOrigin #pickerOrigin=\"cdkOverlayOrigin\">\n <!-- Processing Indicator Overlay -->\n @if (isProcessing) {\n <div class=\"processing-indicator\">\n <i class=\"fas fa-circle-notch fa-spin\"></i>\n <span>{{ processingMessage }}</span>\n </div>\n }\n\n <!-- AI composer (mic + Plan Mode buttons live in its in-composer toolbar): wraps the\n generic mj-message-input-box with the '@' agent / '#' record / '/' skill trigger\n plugins built in \u2014 all three on by default, preserving the standard chat behavior. -->\n <mj-ai-composer\n #inputBox\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled || isProcessing\"\n [showCharacterCount]=\"false\"\n [enableMentions]=\"enableMentions\"\n [EnableAgentMentions]=\"enableAgentMentions\"\n [EnableEntityMentions]=\"enableEntityMentions\"\n [EnableSkillCommands]=\"enableSkillCommands\"\n [TargetAgentId]=\"pickerTargetAgentId\"\n [AllowedAgentIDs]=\"AllowedAgentIDs\"\n [MentionPeople]=\"MentionPeople\"\n [enableAttachments]=\"enableAttachments\"\n [maxAttachments]=\"maxAttachments\"\n [maxAttachmentSizeBytes]=\"maxAttachmentSizeBytes\"\n [acceptedFileTypes]=\"acceptedFileTypes\"\n [currentUser]=\"currentUser\"\n [Provider]=\"Provider\"\n [rows]=\"3\"\n [enableRealtime]=\"enableRealtime\"\n [voiceActive]=\"voiceActive\"\n [canStartRealtime]=\"canStartRealtime\"\n [enablePlanMode]=\"enablePlanMode\"\n [planModeActive]=\"PlanModeEnabled\"\n [SendSlotMode]=\"SendSlotMode\"\n [StopCount]=\"StopCount\"\n [StopLabel]=\"StopLabel\"\n [StatusText]=\"StatusText\"\n [StatusElapsed]=\"StatusElapsed\"\n (StopClicked)=\"StopClicked.emit()\"\n [value]=\"messageText\"\n (valueChange)=\"OnComposerValueChanged($event)\"\n (blurred)=\"ComposerBlurred.emit()\"\n (textSubmitted)=\"onTextSubmitted($event)\"\n (attachmentsChanged)=\"onAttachmentsChanged($event)\"\n (attachmentError)=\"onAttachmentError($event)\"\n (voiceRequested)=\"onStartRealtime()\"\n (voiceOptionsRequested)=\"onRealtimeOptions()\"\n (planModeToggle)=\"TogglePlanMode()\">\n </mj-ai-composer>\n\n <!-- Voice agent/co-agent/model picker \u2014 shown when starting a call on a conversation\n with NO prior agent participation, or on demand via the caret next to the phone\n button (any conversation). Rendered in a body-level CDK overlay anchored above the\n composer so it POPS OUT of the chat overlay's clipping border instead of being cut\n off at the top of a narrow (~390px) overlay. -->\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"pickerOrigin\"\n [cdkConnectedOverlayOpen]=\"showRealtimeAgentPicker\"\n [cdkConnectedOverlayPositions]=\"pickerOverlayPositions\"\n [cdkConnectedOverlayPush]=\"true\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n (backdropClick)=\"onRealtimeAgentPickerCancelled()\"\n (detach)=\"onRealtimeAgentPickerCancelled()\">\n <mj-realtime-agent-picker\n [Provider]=\"ProviderToUse\"\n [CurrentUser]=\"currentUser\"\n [Agents]=\"voicePickerAgents\"\n [DefaultAgentId]=\"voicePickerDefaultAgentId\"\n [CoAgents]=\"voicePickerCoAgents\"\n [DefaultCoAgentId]=\"voicePickerDefaultCoAgentId\"\n (AgentPicked)=\"onRealtimeAgentPicked($event)\"\n (Cancelled)=\"onRealtimeAgentPickerCancelled()\">\n </mj-realtime-agent-picker>\n </ng-template>\n</div>\n\n<!-- The voice \"call mode\" overlay is hosted by <mj-conversation-chat-area> (it fills the\n whole conversation panel in place); this component only owns the trigger wiring. -->\n", styles: [".message-input-wrapper {\n position: relative;\n width: 100%;\n}\n\n/* Plan Mode moved into the in-composer toolbar (message-input-box) as a toggle icon button\n next to the attachment/voice buttons \u2014 see .plan-mode-button-icon there. */\n\n.processing-indicator {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1.25rem;\n background: color-mix(in srgb, var(--mj-bg-surface-card) 95%, transparent);\n border-radius: 8px;\n box-shadow: var(--mj-shadow-sm);\n z-index: 10;\n pointer-events: none;\n}\n.processing-indicator i {\n color: var(--mj-brand-primary);\n}\n.processing-indicator span {\n font-size: 0.9rem;\n color: var(--mj-text-primary);\n}\n"] }]
3554
+ args: [{ standalone: false, selector: 'mj-message-input', template: "<div class=\"message-input-wrapper\" cdkOverlayOrigin #pickerOrigin=\"cdkOverlayOrigin\">\n <!-- Processing Indicator Overlay -->\n @if (isProcessing) {\n <div class=\"processing-indicator\">\n <i class=\"fas fa-circle-notch fa-spin\"></i>\n <span>{{ processingMessage }}</span>\n </div>\n }\n\n @if (!emptyStateMode && CurrentRouteAgent) {\n <div class=\"composer-route route\" [style.--agent-accent]=\"CurrentRouteAccentVar\">\n <span class=\"to\">\n <span class=\"dot\" aria-hidden=\"true\"></span>\n <span>Replying to <b>{{ CurrentRouteAgentName }}</b></span>\n </span>\n @if (ShowReturnToSage) {\n <button type=\"button\" class=\"linkish\" (click)=\"ReturnToSage()\">\n Return to {{ ConverationManagerAgent?.Name || 'Sage' }}\n </button>\n }\n </div>\n }\n\n <!-- AI composer (mic + Plan Mode buttons live in its in-composer toolbar): wraps the\n generic mj-message-input-box with the '@' agent / '#' record / '/' skill trigger\n plugins built in \u2014 all three on by default, preserving the standard chat behavior. -->\n <mj-ai-composer\n #inputBox\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled || isProcessing\"\n [showCharacterCount]=\"false\"\n [enableMentions]=\"enableMentions\"\n [EnableAgentMentions]=\"enableAgentMentions\"\n [EnableEntityMentions]=\"enableEntityMentions\"\n [EnableSkillCommands]=\"enableSkillCommands\"\n [TargetAgentId]=\"pickerTargetAgentId\"\n [AllowedAgentIDs]=\"AllowedAgentIDs\"\n [MentionPeople]=\"MentionPeople\"\n [enableAttachments]=\"enableAttachments\"\n [maxAttachments]=\"maxAttachments\"\n [maxAttachmentSizeBytes]=\"maxAttachmentSizeBytes\"\n [acceptedFileTypes]=\"acceptedFileTypes\"\n [currentUser]=\"currentUser\"\n [Provider]=\"Provider\"\n [rows]=\"3\"\n [enableRealtime]=\"enableRealtime\"\n [voiceActive]=\"voiceActive\"\n [canStartRealtime]=\"canStartRealtime\"\n [enablePlanMode]=\"enablePlanMode\"\n [planModeActive]=\"PlanModeEnabled\"\n [SendSlotMode]=\"SendSlotMode\"\n [StopCount]=\"StopCount\"\n [StopLabel]=\"StopLabel\"\n [StatusText]=\"StatusText\"\n [StatusElapsed]=\"StatusElapsed\"\n (StopClicked)=\"StopClicked.emit()\"\n [value]=\"messageText\"\n (valueChange)=\"OnComposerValueChanged($event)\"\n (blurred)=\"ComposerBlurred.emit()\"\n (textSubmitted)=\"onTextSubmitted($event)\"\n (attachmentsChanged)=\"onAttachmentsChanged($event)\"\n (attachmentError)=\"onAttachmentError($event)\"\n (voiceRequested)=\"onStartRealtime()\"\n (voiceOptionsRequested)=\"onRealtimeOptions()\"\n (planModeToggle)=\"TogglePlanMode()\">\n </mj-ai-composer>\n\n <!-- Voice agent/co-agent/model picker \u2014 shown when starting a call on a conversation\n with NO prior agent participation, or on demand via the caret next to the phone\n button (any conversation). Rendered in a body-level CDK overlay anchored above the\n composer so it POPS OUT of the chat overlay's clipping border instead of being cut\n off at the top of a narrow (~390px) overlay. -->\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"pickerOrigin\"\n [cdkConnectedOverlayOpen]=\"showRealtimeAgentPicker\"\n [cdkConnectedOverlayPositions]=\"pickerOverlayPositions\"\n [cdkConnectedOverlayPush]=\"true\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n (backdropClick)=\"onRealtimeAgentPickerCancelled()\"\n (detach)=\"onRealtimeAgentPickerCancelled()\">\n <mj-realtime-agent-picker\n [Provider]=\"ProviderToUse\"\n [CurrentUser]=\"currentUser\"\n [Agents]=\"voicePickerAgents\"\n [DefaultAgentId]=\"voicePickerDefaultAgentId\"\n [CoAgents]=\"voicePickerCoAgents\"\n [DefaultCoAgentId]=\"voicePickerDefaultCoAgentId\"\n (AgentPicked)=\"onRealtimeAgentPicked($event)\"\n (Cancelled)=\"onRealtimeAgentPickerCancelled()\">\n </mj-realtime-agent-picker>\n </ng-template>\n</div>\n\n<!-- The voice \"call mode\" overlay is hosted by <mj-conversation-chat-area> (it fills the\n whole conversation panel in place); this component only owns the trigger wiring. -->\n", styles: [".message-input-wrapper {\n position: relative;\n width: 100%;\n}\n\n/* Plan Mode moved into the in-composer toolbar (message-input-box) as a toggle icon button\n next to the attachment/voice buttons \u2014 see .plan-mode-button-icon there. */\n\n.processing-indicator {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1.25rem;\n background: color-mix(in srgb, var(--mj-bg-surface-card) 95%, transparent);\n border-radius: 8px;\n box-shadow: var(--mj-shadow-sm);\n z-index: 10;\n pointer-events: none;\n}\n.processing-indicator i {\n color: var(--mj-brand-primary);\n}\n.processing-indicator span {\n font-size: 0.9rem;\n color: var(--mj-text-primary);\n}\n\n.composer-route {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 10px 6px;\n font-size: 11.5px;\n color: var(--mj-text-muted);\n}\n\n.composer-route .to {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n\n.composer-route .to b {\n font-weight: 600;\n color: var(--mj-text-secondary);\n}\n\n.composer-route .dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--agent-accent, var(--mj-brand-primary));\n flex-shrink: 0;\n transition: background-color 240ms cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.composer-route .linkish {\n margin-left: auto;\n padding: 2px 4px;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: var(--mj-brand-primary);\n font-family: inherit;\n font-size: 11.5px;\n font-weight: 500;\n text-decoration: underline;\n text-decoration-style: dotted;\n text-decoration-thickness: 1px;\n text-underline-offset: 2px;\n cursor: pointer;\n}\n\n.composer-route .linkish:hover {\n color: var(--mj-brand-primary-hover);\n}\n\n.composer-route .linkish:focus-visible {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n box-shadow: var(--mj-focus-ring, none);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .composer-route .dot {\n transition: none;\n }\n}\n\n"] }]
3550
3555
  }], () => [{ type: i1.DialogService }, { type: i2.ToastService }, { type: i3.ConversationAgentService }, { type: i4.DataCacheService }, { type: i5.ActiveTasksService }, { type: i6.ConversationStreamingService }, { type: i7.MentionParserService }, { type: i8.ConversationAttachmentService }, { type: i9.ConversationBridgeService }, { type: i10.RealtimeSessionService }], { ConversationId: [{
3551
3556
  type: Input
3552
3557
  }], conversationId: [{
@@ -3773,5 +3778,5 @@ export class MessageInputComponent extends BaseAngularComponent {
3773
3778
  }], ComposerBlurred: [{
3774
3779
  type: Output
3775
3780
  }] }); })();
3776
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MessageInputComponent, { className: "MessageInputComponent", filePath: "src/lib/components/message/message-input.component.ts", lineNumber: 71 }); })();
3781
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MessageInputComponent, { className: "MessageInputComponent", filePath: "src/lib/components/message/message-input.component.ts", lineNumber: 72 }); })();
3777
3782
  //# sourceMappingURL=message-input.component.js.map