@meetsmore-oss/use-ai-client 1.13.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -99,6 +99,11 @@ var defaultStrings = {
99
99
  /** Error for unknown/unexpected errors */
100
100
  UNKNOWN_ERROR: "An unexpected error occurred. Please try again."
101
101
  },
102
+ // Non-error notices shown as system-style bubbles
103
+ notices: {
104
+ /** Shown as a separate bubble after the user aborts generation */
105
+ aborted: "Generation stopped. You can continue the conversation."
106
+ },
102
107
  // Thinking/reasoning display
103
108
  thinking: {
104
109
  /** Label shown while thinking is in progress */
@@ -157,6 +162,8 @@ var defaultTheme = {
157
162
  activeBackground: "#f0f0ff",
158
163
  /** Disabled button background */
159
164
  buttonDisabledBackground: "#e5e7eb",
165
+ /** Stop (abort) button background — neutral so it doesn't read as a primary action */
166
+ stopButtonBackground: "#e5e7eb",
160
167
  // Text colors
161
168
  /** Primary text color */
162
169
  textColor: "#1f2937",
@@ -301,10 +308,29 @@ function mergeAssistantMessagesForDisplay(messages) {
301
308
  let pendingTexts = [];
302
309
  let pendingIds = [];
303
310
  let pendingReasoningParts = [];
311
+ const flushPending = () => {
312
+ if (pendingTexts.length > 0 || pendingReasoningParts.length > 0) {
313
+ result.push({
314
+ id: `merged-${pendingIds.join("-")}`,
315
+ role: "assistant",
316
+ content: pendingTexts.join("\n\n"),
317
+ createdAt: /* @__PURE__ */ new Date(),
318
+ ...pendingReasoningParts.length > 0 ? { reasoningParts: pendingReasoningParts } : {}
319
+ });
320
+ pendingTexts = [];
321
+ pendingIds = [];
322
+ pendingReasoningParts = [];
323
+ }
324
+ };
304
325
  for (const msg of messages) {
305
326
  if (msg.role === "tool") {
306
327
  continue;
307
328
  }
329
+ if (msg.displayMode === "info") {
330
+ flushPending();
331
+ result.push(msg);
332
+ continue;
333
+ }
308
334
  if (msg.role === "assistant") {
309
335
  const text = getTextFromContent(msg.content);
310
336
  if (msg.toolCalls && msg.toolCalls.length > 0) {
@@ -329,30 +355,11 @@ function mergeAssistantMessagesForDisplay(messages) {
329
355
  pendingReasoningParts = [];
330
356
  }
331
357
  } else {
332
- if (pendingTexts.length > 0 || pendingReasoningParts.length > 0) {
333
- result.push({
334
- id: `merged-${pendingIds.join("-")}`,
335
- role: "assistant",
336
- content: pendingTexts.join("\n\n"),
337
- createdAt: /* @__PURE__ */ new Date(),
338
- ...pendingReasoningParts.length > 0 ? { reasoningParts: pendingReasoningParts } : {}
339
- });
340
- pendingTexts = [];
341
- pendingIds = [];
342
- pendingReasoningParts = [];
343
- }
358
+ flushPending();
344
359
  result.push(msg);
345
360
  }
346
361
  }
347
- if (pendingTexts.length > 0 || pendingReasoningParts.length > 0) {
348
- result.push({
349
- id: `merged-${pendingIds.join("-")}`,
350
- role: "assistant",
351
- content: pendingTexts.join("\n\n"),
352
- createdAt: /* @__PURE__ */ new Date(),
353
- ...pendingReasoningParts.length > 0 ? { reasoningParts: pendingReasoningParts } : {}
354
- });
355
- }
362
+ flushPending();
356
363
  return result;
357
364
  }
358
365
 
@@ -2161,6 +2168,7 @@ function hasFileContent(content) {
2161
2168
  }
2162
2169
  function UseAIChatPanel({
2163
2170
  onSendMessage,
2171
+ onAbort,
2164
2172
  messages,
2165
2173
  loading,
2166
2174
  connected,
@@ -2718,177 +2726,207 @@ function UseAIChatPanel({
2718
2726
  ]
2719
2727
  }
2720
2728
  ),
2721
- displayMessages.map((message) => /* @__PURE__ */ jsxs9(
2722
- "div",
2723
- {
2724
- "data-testid": `chat-message-${message.role}`,
2725
- className: `chat-message chat-message-${message.role}`,
2726
- style: {
2727
- display: "flex",
2728
- flexDirection: "column",
2729
- alignItems: message.role === "user" ? "flex-end" : "flex-start"
2730
- },
2731
- onMouseEnter: () => message.role === "user" && setHoveredMessageId(message.id),
2732
- onMouseLeave: () => setHoveredMessageId(null),
2733
- children: [
2734
- /* @__PURE__ */ jsxs9(
2735
- "div",
2736
- {
2737
- style: {
2738
- position: "relative",
2739
- maxWidth: "80%"
2740
- },
2741
- children: [
2742
- message.role === "user" && hoveredMessageId === message.id && onSaveCommand && !slashCommands.isSavingCommand(message.id) && /* @__PURE__ */ jsx12(
2743
- "button",
2744
- {
2745
- "data-testid": "save-command-button",
2746
- onClick: (e) => {
2747
- e.stopPropagation();
2748
- const messageText = getDisplayTextFromContent(message.content);
2749
- slashCommands.startSavingCommand(message.id, messageText);
2750
- },
2751
- title: "Save as slash command",
2752
- style: {
2753
- position: "absolute",
2754
- top: "-8px",
2755
- right: "-8px",
2756
- width: "24px",
2757
- height: "24px",
2758
- borderRadius: "50%",
2759
- border: "none",
2760
- background: theme.backgroundColor,
2761
- boxShadow: "0 2px 6px rgba(0, 0, 0, 0.15)",
2762
- cursor: "pointer",
2763
- display: "flex",
2764
- alignItems: "center",
2765
- justifyContent: "center",
2766
- color: theme.primaryColor,
2767
- transition: "all 0.15s",
2768
- zIndex: 10
2769
- },
2770
- onMouseEnter: (e) => {
2771
- e.currentTarget.style.transform = "scale(1.1)";
2772
- e.currentTarget.style.boxShadow = "0 3px 8px rgba(0, 0, 0, 0.2)";
2773
- },
2774
- onMouseLeave: (e) => {
2775
- e.currentTarget.style.transform = "scale(1)";
2776
- e.currentTarget.style.boxShadow = "0 2px 6px rgba(0, 0, 0, 0.15)";
2777
- },
2778
- children: /* @__PURE__ */ jsxs9("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2779
- /* @__PURE__ */ jsx12("path", { d: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" }),
2780
- /* @__PURE__ */ jsx12("polyline", { points: "17 21 17 13 7 13 7 21" }),
2781
- /* @__PURE__ */ jsx12("polyline", { points: "7 3 7 8 15 8" })
2782
- ] })
2783
- }
2784
- ),
2785
- /* @__PURE__ */ jsxs9(
2786
- "div",
2787
- {
2788
- "data-testid": "chat-message-content",
2789
- className: `chat-message-content${message.role === "assistant" ? " markdown-content" : ""}`,
2790
- style: {
2791
- padding: "10px 14px",
2792
- borderRadius: slashCommands.isSavingCommand(message.id) ? "12px 12px 0 0" : "12px",
2793
- background: message.displayMode === "error" ? theme.errorBackground : message.role === "user" ? theme.primaryGradient : theme.assistantMessageBackground,
2794
- color: message.displayMode === "error" ? theme.errorTextColor : message.role === "user" ? "white" : theme.textColor,
2795
- fontSize: "14px",
2796
- lineHeight: "1.5",
2797
- wordWrap: "break-word"
2798
- },
2799
- children: [
2800
- message.role === "user" && hasFileContent(message.content) && /* @__PURE__ */ jsx12("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px", marginBottom: "8px" }, children: message.content.filter((part) => part.type === "file").map((part, idx) => /* @__PURE__ */ jsx12(
2801
- FilePlaceholder,
2802
- {
2803
- name: part.file.name,
2804
- size: part.file.size
2805
- },
2806
- idx
2807
- )) }),
2808
- message.role === "assistant" ? /* @__PURE__ */ jsxs9(Fragment, { children: [
2809
- message.reasoningParts && message.reasoningParts.length > 0 && /* @__PURE__ */ jsx12(
2810
- Reasoning,
2729
+ displayMessages.map((message) => {
2730
+ if (message.displayMode === "info") {
2731
+ return /* @__PURE__ */ jsx12(
2732
+ "div",
2733
+ {
2734
+ "data-testid": "chat-message-info",
2735
+ className: "chat-message chat-message-info",
2736
+ style: { display: "flex", justifyContent: "center", padding: "2px 0" },
2737
+ children: /* @__PURE__ */ jsx12(
2738
+ "div",
2739
+ {
2740
+ style: {
2741
+ maxWidth: "80%",
2742
+ padding: "6px 12px",
2743
+ borderRadius: "12px",
2744
+ background: theme.assistantMessageBackground,
2745
+ color: theme.secondaryTextColor,
2746
+ fontSize: "12px",
2747
+ lineHeight: "1.4",
2748
+ textAlign: "center",
2749
+ wordWrap: "break-word"
2750
+ },
2751
+ children: getDisplayTextFromContent(message.content)
2752
+ }
2753
+ )
2754
+ },
2755
+ message.id
2756
+ );
2757
+ }
2758
+ return /* @__PURE__ */ jsxs9(
2759
+ "div",
2760
+ {
2761
+ "data-testid": `chat-message-${message.role}`,
2762
+ className: `chat-message chat-message-${message.role}`,
2763
+ style: {
2764
+ display: "flex",
2765
+ flexDirection: "column",
2766
+ alignItems: message.role === "user" ? "flex-end" : "flex-start"
2767
+ },
2768
+ onMouseEnter: () => message.role === "user" && setHoveredMessageId(message.id),
2769
+ onMouseLeave: () => setHoveredMessageId(null),
2770
+ children: [
2771
+ /* @__PURE__ */ jsxs9(
2772
+ "div",
2773
+ {
2774
+ style: {
2775
+ position: "relative",
2776
+ maxWidth: "80%"
2777
+ },
2778
+ children: [
2779
+ message.role === "user" && hoveredMessageId === message.id && onSaveCommand && !slashCommands.isSavingCommand(message.id) && /* @__PURE__ */ jsx12(
2780
+ "button",
2781
+ {
2782
+ "data-testid": "save-command-button",
2783
+ onClick: (e) => {
2784
+ e.stopPropagation();
2785
+ const messageText = getDisplayTextFromContent(message.content);
2786
+ slashCommands.startSavingCommand(message.id, messageText);
2787
+ },
2788
+ title: "Save as slash command",
2789
+ style: {
2790
+ position: "absolute",
2791
+ top: "-8px",
2792
+ right: "-8px",
2793
+ width: "24px",
2794
+ height: "24px",
2795
+ borderRadius: "50%",
2796
+ border: "none",
2797
+ background: theme.backgroundColor,
2798
+ boxShadow: "0 2px 6px rgba(0, 0, 0, 0.15)",
2799
+ cursor: "pointer",
2800
+ display: "flex",
2801
+ alignItems: "center",
2802
+ justifyContent: "center",
2803
+ color: theme.primaryColor,
2804
+ transition: "all 0.15s",
2805
+ zIndex: 10
2806
+ },
2807
+ onMouseEnter: (e) => {
2808
+ e.currentTarget.style.transform = "scale(1.1)";
2809
+ e.currentTarget.style.boxShadow = "0 3px 8px rgba(0, 0, 0, 0.2)";
2810
+ },
2811
+ onMouseLeave: (e) => {
2812
+ e.currentTarget.style.transform = "scale(1)";
2813
+ e.currentTarget.style.boxShadow = "0 2px 6px rgba(0, 0, 0, 0.15)";
2814
+ },
2815
+ children: /* @__PURE__ */ jsxs9("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2816
+ /* @__PURE__ */ jsx12("path", { d: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" }),
2817
+ /* @__PURE__ */ jsx12("polyline", { points: "17 21 17 13 7 13 7 21" }),
2818
+ /* @__PURE__ */ jsx12("polyline", { points: "7 3 7 8 15 8" })
2819
+ ] })
2820
+ }
2821
+ ),
2822
+ /* @__PURE__ */ jsxs9(
2823
+ "div",
2824
+ {
2825
+ "data-testid": "chat-message-content",
2826
+ className: `chat-message-content${message.role === "assistant" ? " markdown-content" : ""}`,
2827
+ style: {
2828
+ padding: "10px 14px",
2829
+ borderRadius: slashCommands.isSavingCommand(message.id) ? "12px 12px 0 0" : "12px",
2830
+ background: message.displayMode === "error" ? theme.errorBackground : message.role === "user" ? theme.primaryGradient : theme.assistantMessageBackground,
2831
+ color: message.displayMode === "error" ? theme.errorTextColor : message.role === "user" ? "white" : theme.textColor,
2832
+ fontSize: "14px",
2833
+ lineHeight: "1.5",
2834
+ wordWrap: "break-word"
2835
+ },
2836
+ children: [
2837
+ message.role === "user" && hasFileContent(message.content) && /* @__PURE__ */ jsx12("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px", marginBottom: "8px" }, children: message.content.filter((part) => part.type === "file").map((part, idx) => /* @__PURE__ */ jsx12(
2838
+ FilePlaceholder,
2811
2839
  {
2812
- reasoningParts: message.reasoningParts,
2813
- theme,
2814
- strings
2815
- }
2816
- ),
2817
- /* @__PURE__ */ jsx12(MarkdownContent, { content: getTextFromContent(message.content) })
2818
- ] }) : (
2819
- // User/tool bubbles: display-only text so transformed_file
2820
- // (e.g. OCR body) isn't dumped into the chat bubble.
2821
- getDisplayTextFromContent(message.content)
2822
- )
2823
- ]
2824
- }
2825
- ),
2826
- slashCommands.renderInlineSaveUI({
2827
- messageId: message.id,
2828
- messageText: getDisplayTextFromContent(message.content)
2840
+ name: part.file.name,
2841
+ size: part.file.size
2842
+ },
2843
+ idx
2844
+ )) }),
2845
+ message.role === "assistant" ? /* @__PURE__ */ jsxs9(Fragment, { children: [
2846
+ message.reasoningParts && message.reasoningParts.length > 0 && /* @__PURE__ */ jsx12(
2847
+ Reasoning,
2848
+ {
2849
+ reasoningParts: message.reasoningParts,
2850
+ theme,
2851
+ strings
2852
+ }
2853
+ ),
2854
+ /* @__PURE__ */ jsx12(MarkdownContent, { content: getTextFromContent(message.content) })
2855
+ ] }) : (
2856
+ // User/tool bubbles: display-only text so transformed_file
2857
+ // (e.g. OCR body) isn't dumped into the chat bubble.
2858
+ getDisplayTextFromContent(message.content)
2859
+ )
2860
+ ]
2861
+ }
2862
+ ),
2863
+ slashCommands.renderInlineSaveUI({
2864
+ messageId: message.id,
2865
+ messageText: getDisplayTextFromContent(message.content)
2866
+ })
2867
+ ]
2868
+ }
2869
+ ),
2870
+ message.role === "assistant" && message.traceId && feedbackEnabled && onFeedback && /* @__PURE__ */ jsxs9(
2871
+ "div",
2872
+ {
2873
+ "data-testid": "feedback-buttons",
2874
+ style: {
2875
+ display: "flex",
2876
+ gap: "4px",
2877
+ marginTop: "4px",
2878
+ padding: "0 4px"
2879
+ },
2880
+ children: [
2881
+ /* @__PURE__ */ jsx12(
2882
+ FeedbackButton,
2883
+ {
2884
+ type: "upvote",
2885
+ isSelected: message.feedback === "upvote",
2886
+ onClick: () => {
2887
+ const newFeedback = message.feedback === "upvote" ? null : "upvote";
2888
+ onFeedback(message.id, message.traceId, newFeedback);
2889
+ },
2890
+ selectedColor: theme.primaryColor,
2891
+ unselectedColor: theme.secondaryTextColor
2892
+ }
2893
+ ),
2894
+ /* @__PURE__ */ jsx12(
2895
+ FeedbackButton,
2896
+ {
2897
+ type: "downvote",
2898
+ isSelected: message.feedback === "downvote",
2899
+ onClick: () => {
2900
+ const newFeedback = message.feedback === "downvote" ? null : "downvote";
2901
+ onFeedback(message.id, message.traceId, newFeedback);
2902
+ },
2903
+ selectedColor: theme.errorTextColor,
2904
+ unselectedColor: theme.secondaryTextColor
2905
+ }
2906
+ )
2907
+ ]
2908
+ }
2909
+ ),
2910
+ /* @__PURE__ */ jsx12(
2911
+ "div",
2912
+ {
2913
+ style: {
2914
+ fontSize: "11px",
2915
+ color: theme.secondaryTextColor,
2916
+ marginTop: "4px",
2917
+ padding: "0 4px"
2918
+ },
2919
+ children: message.createdAt.toLocaleTimeString([], {
2920
+ hour: "2-digit",
2921
+ minute: "2-digit"
2829
2922
  })
2830
- ]
2831
- }
2832
- ),
2833
- message.role === "assistant" && message.traceId && feedbackEnabled && onFeedback && /* @__PURE__ */ jsxs9(
2834
- "div",
2835
- {
2836
- "data-testid": "feedback-buttons",
2837
- style: {
2838
- display: "flex",
2839
- gap: "4px",
2840
- marginTop: "4px",
2841
- padding: "0 4px"
2842
- },
2843
- children: [
2844
- /* @__PURE__ */ jsx12(
2845
- FeedbackButton,
2846
- {
2847
- type: "upvote",
2848
- isSelected: message.feedback === "upvote",
2849
- onClick: () => {
2850
- const newFeedback = message.feedback === "upvote" ? null : "upvote";
2851
- onFeedback(message.id, message.traceId, newFeedback);
2852
- },
2853
- selectedColor: theme.primaryColor,
2854
- unselectedColor: theme.secondaryTextColor
2855
- }
2856
- ),
2857
- /* @__PURE__ */ jsx12(
2858
- FeedbackButton,
2859
- {
2860
- type: "downvote",
2861
- isSelected: message.feedback === "downvote",
2862
- onClick: () => {
2863
- const newFeedback = message.feedback === "downvote" ? null : "downvote";
2864
- onFeedback(message.id, message.traceId, newFeedback);
2865
- },
2866
- selectedColor: theme.errorTextColor,
2867
- unselectedColor: theme.secondaryTextColor
2868
- }
2869
- )
2870
- ]
2871
- }
2872
- ),
2873
- /* @__PURE__ */ jsx12(
2874
- "div",
2875
- {
2876
- style: {
2877
- fontSize: "11px",
2878
- color: theme.secondaryTextColor,
2879
- marginTop: "4px",
2880
- padding: "0 4px"
2881
- },
2882
- children: message.createdAt.toLocaleTimeString([], {
2883
- hour: "2-digit",
2884
- minute: "2-digit"
2885
- })
2886
- }
2887
- )
2888
- ]
2889
- },
2890
- message.id
2891
- )),
2923
+ }
2924
+ )
2925
+ ]
2926
+ },
2927
+ message.id
2928
+ );
2929
+ }),
2892
2930
  loading && /* @__PURE__ */ jsx12(
2893
2931
  "div",
2894
2932
  {
@@ -3113,33 +3151,65 @@ function UseAIChatPanel({
3113
3151
  ] })
3114
3152
  }
3115
3153
  ) }),
3116
- /* @__PURE__ */ jsx12(
3117
- "button",
3118
- {
3119
- "data-testid": "chat-send-button",
3120
- className: "chat-send-button",
3121
- onClick: handleSend,
3122
- disabled: !connected || loading || pendingApprovals.length > 0 || !input.trim() && attachments.length === 0,
3123
- style: {
3124
- padding: "6px",
3125
- background: connected && !loading && pendingApprovals.length === 0 && (input.trim() || attachments.length > 0) ? theme.primaryGradient : theme.buttonDisabledBackground,
3126
- color: connected && !loading && pendingApprovals.length === 0 && (input.trim() || attachments.length > 0) ? "white" : theme.secondaryTextColor,
3127
- border: "none",
3128
- borderRadius: "50%",
3129
- cursor: connected && !loading && pendingApprovals.length === 0 && (input.trim() || attachments.length > 0) ? "pointer" : "not-allowed",
3130
- display: "flex",
3131
- alignItems: "center",
3132
- justifyContent: "center",
3133
- width: "32px",
3134
- height: "32px",
3135
- transition: "all 0.2s"
3136
- },
3137
- children: /* @__PURE__ */ jsxs9("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
3138
- /* @__PURE__ */ jsx12("line", { x1: "12", y1: "19", x2: "12", y2: "5" }),
3139
- /* @__PURE__ */ jsx12("polyline", { points: "5 12 12 5 19 12" })
3140
- ] })
3154
+ (() => {
3155
+ const canAbort = loading && !!onAbort;
3156
+ const canSend = connected && !loading && pendingApprovals.length === 0 && (input.trim() || attachments.length > 0);
3157
+ if (loading && onAbort) {
3158
+ return /* @__PURE__ */ jsx12(
3159
+ "button",
3160
+ {
3161
+ "data-testid": "chat-stop-button",
3162
+ className: "chat-stop-button",
3163
+ onClick: onAbort,
3164
+ disabled: !canAbort,
3165
+ title: canAbort ? "Stop generating" : "Cannot stop while a tool is running",
3166
+ "aria-label": "Stop generating",
3167
+ style: {
3168
+ padding: "6px",
3169
+ background: canAbort ? theme.stopButtonBackground : theme.buttonDisabledBackground,
3170
+ color: theme.secondaryTextColor,
3171
+ border: "none",
3172
+ borderRadius: "50%",
3173
+ cursor: canAbort ? "pointer" : "not-allowed",
3174
+ display: "flex",
3175
+ alignItems: "center",
3176
+ justifyContent: "center",
3177
+ width: "32px",
3178
+ height: "32px",
3179
+ transition: "all 0.2s"
3180
+ },
3181
+ children: /* @__PURE__ */ jsx12("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", stroke: "none", children: /* @__PURE__ */ jsx12("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) })
3182
+ }
3183
+ );
3141
3184
  }
3142
- )
3185
+ return /* @__PURE__ */ jsx12(
3186
+ "button",
3187
+ {
3188
+ "data-testid": "chat-send-button",
3189
+ className: "chat-send-button",
3190
+ onClick: handleSend,
3191
+ disabled: !canSend,
3192
+ style: {
3193
+ padding: "6px",
3194
+ background: canSend ? theme.primaryGradient : theme.buttonDisabledBackground,
3195
+ color: canSend ? "white" : theme.secondaryTextColor,
3196
+ border: "none",
3197
+ borderRadius: "50%",
3198
+ cursor: canSend ? "pointer" : "not-allowed",
3199
+ display: "flex",
3200
+ alignItems: "center",
3201
+ justifyContent: "center",
3202
+ width: "32px",
3203
+ height: "32px",
3204
+ transition: "all 0.2s"
3205
+ },
3206
+ children: /* @__PURE__ */ jsxs9("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
3207
+ /* @__PURE__ */ jsx12("line", { x1: "12", y1: "19", x2: "12", y2: "5" }),
3208
+ /* @__PURE__ */ jsx12("polyline", { points: "5 12 12 5 19 12" })
3209
+ ] })
3210
+ }
3211
+ );
3212
+ })()
3143
3213
  ]
3144
3214
  }
3145
3215
  )
@@ -3317,7 +3387,8 @@ function UseAIChat({ floating = false, submitMode }) {
3317
3387
  onFeedback: ctx.feedback?.submit,
3318
3388
  pendingApprovals: ctx.tools.pending.tools,
3319
3389
  onApproveToolCall: ctx.tools.pending.tools.length > 0 ? ctx.tools.pending.approveAll : void 0,
3320
- onRejectToolCall: ctx.tools.pending.tools.length > 0 ? ctx.tools.pending.rejectAll : void 0
3390
+ onRejectToolCall: ctx.tools.pending.tools.length > 0 ? ctx.tools.pending.rejectAll : void 0,
3391
+ onAbort: ctx.abortRun
3321
3392
  };
3322
3393
  if (floating) {
3323
3394
  return /* @__PURE__ */ jsx14(
@@ -3364,6 +3435,9 @@ var UseAIClient = class {
3364
3435
  _tools = [];
3365
3436
  _messages = [];
3366
3437
  _state = null;
3438
+ // Tracks the in-flight run so abortRun() can target it. Set by sendPrompt
3439
+ // and cleared at RUN_FINISHED / RUN_ERROR.
3440
+ _currentRunId = null;
3367
3441
  // Agent selection
3368
3442
  _availableAgents = [];
3369
3443
  _defaultAgent = null;
@@ -3453,6 +3527,7 @@ var UseAIClient = class {
3453
3527
  this._pendingToolResults = [];
3454
3528
  this._currentReasoningBlocks = [];
3455
3529
  this._currentReasoningBlockText = "";
3530
+ this._currentMessageContent = "";
3456
3531
  }
3457
3532
  if (event.type === EventType.TEXT_MESSAGE_START) {
3458
3533
  const e = event;
@@ -3543,38 +3618,10 @@ var UseAIClient = class {
3543
3618
  this._currentReasoningBlocks = [];
3544
3619
  }
3545
3620
  } else if (event.type === EventType.RUN_FINISHED) {
3546
- if (this._currentAssistantMessage) {
3547
- const reasoningParts = this._currentReasoningBlocks.length > 0 ? [...this._currentReasoningBlocks] : void 0;
3548
- if (this._currentAssistantToolCalls.length > 0) {
3549
- const toolCallMessage = {
3550
- id: uuidv42(),
3551
- role: "assistant",
3552
- content: "",
3553
- toolCalls: this._currentAssistantToolCalls,
3554
- ...reasoningParts ? { reasoningParts } : {}
3555
- };
3556
- this._messages.push(toolCallMessage);
3557
- this._messages.push(...this._pendingToolResults);
3558
- const textMessage = {
3559
- id: this._currentAssistantMessage.id,
3560
- role: "assistant",
3561
- content: this._currentAssistantMessage.content || ""
3562
- };
3563
- this._messages.push(textMessage);
3564
- } else {
3565
- const assistantMessage = {
3566
- id: this._currentAssistantMessage.id,
3567
- role: "assistant",
3568
- content: this._currentAssistantMessage.content || "",
3569
- ...reasoningParts ? { reasoningParts } : {}
3570
- };
3571
- this._messages.push(assistantMessage);
3572
- }
3573
- this._currentAssistantMessage = null;
3574
- this._currentAssistantToolCalls = [];
3575
- this._pendingToolResults = [];
3576
- this._currentReasoningBlockText = "";
3577
- }
3621
+ this.finalizeRun({ aborted: false });
3622
+ }
3623
+ if (event.type === EventType.RUN_FINISHED || event.type === EventType.RUN_ERROR) {
3624
+ this._currentRunId = null;
3578
3625
  }
3579
3626
  this.eventHandlers.forEach((handler) => handler(event));
3580
3627
  }
@@ -3638,10 +3685,12 @@ var UseAIClient = class {
3638
3685
  // Type cast needed for Message type compatibility
3639
3686
  };
3640
3687
  this._messages.push(userMessage);
3688
+ const runId = uuidv42();
3689
+ this._currentRunId = runId;
3641
3690
  const runInput = {
3642
3691
  threadId: this.threadId,
3643
3692
  // Use getter to ensure non-null
3644
- runId: uuidv42(),
3693
+ runId,
3645
3694
  messages: this._messages,
3646
3695
  tools: this._tools.map((t) => ({
3647
3696
  name: t.name,
@@ -3696,6 +3745,115 @@ var UseAIClient = class {
3696
3745
  this._pendingToolResults.push(toolResultMsg);
3697
3746
  this.send(toolResultMessage);
3698
3747
  }
3748
+ /**
3749
+ * Aborts the in-flight run, if any.
3750
+ * Sends an `abort_run` message to the server which cancels the AI stream
3751
+ * and rejects any pending tool/approval waits. The server then emits
3752
+ * `RUN_ERROR` with `ErrorCode.ABORTED`, which the client handles by
3753
+ * persisting the partial response.
3754
+ *
3755
+ * No-op when no run is in flight.
3756
+ */
3757
+ abortRun() {
3758
+ const runId = this._currentRunId;
3759
+ if (!runId) return;
3760
+ this.send({
3761
+ type: "abort_run",
3762
+ data: { runId }
3763
+ });
3764
+ }
3765
+ /**
3766
+ * Flushes the final in-progress step into `_messages` when a run terminates.
3767
+ *
3768
+ * Only two terminations reach here. Truncation (maxOutputTokens / finish
3769
+ * reason 'length') and tool-execution errors never do — the server absorbs
3770
+ * them: truncation continues via a fallback step and ends as a normal
3771
+ * RUN_FINISHED, and tool errors come back as tool_results that keep the run
3772
+ * going. So the dispatch is binary:
3773
+ * - `aborted: false` (RUN_FINISHED): every tool-call step was already flushed
3774
+ * at STEP_FINISHED, so the in-progress step is always text-only.
3775
+ * - `aborted: true` (RUN_ERROR / ABORTED): the run may have been cut
3776
+ * mid-tool-call or mid-reasoning, so extra repair is needed.
3777
+ *
3778
+ * After this returns, `currentMessageContent` and `currentReasoningBlocks`
3779
+ * remain readable for the persistence helper. The next RUN_STARTED clears them.
3780
+ */
3781
+ finalizeRun(opts) {
3782
+ if (opts.aborted) {
3783
+ this.finalizeAbortedRun();
3784
+ } else {
3785
+ this.finalizeCompletedRun();
3786
+ }
3787
+ }
3788
+ /**
3789
+ * Normal completion (RUN_FINISHED). The in-progress step is text-only —
3790
+ * tool-call steps were already flushed at STEP_FINISHED — so just push the
3791
+ * trailing assistant text with its reasoning.
3792
+ */
3793
+ finalizeCompletedRun() {
3794
+ this._currentReasoningBlockText = "";
3795
+ if (!this._currentAssistantMessage) return;
3796
+ if (this._currentMessageContent) {
3797
+ const stepReasoning = this._currentReasoningBlocks.length > 0 ? [...this._currentReasoningBlocks] : void 0;
3798
+ this._messages.push({
3799
+ id: this._currentAssistantMessage.id || uuidv42(),
3800
+ role: "assistant",
3801
+ content: this._currentMessageContent,
3802
+ ...stepReasoning ? { reasoningParts: stepReasoning } : {}
3803
+ });
3804
+ }
3805
+ this._currentAssistantMessage = null;
3806
+ this._currentAssistantToolCalls = [];
3807
+ this._pendingToolResults = [];
3808
+ }
3809
+ /**
3810
+ * User-initiated abort (RUN_ERROR / ABORTED).
3811
+ *
3812
+ * Drops the in-progress step's reasoning blocks. A block gets its encrypted
3813
+ * signature on REASONING_ENCRYPTED_VALUE, which arrives after
3814
+ * REASONING_MESSAGE_END — so a mid-stream abort can leave signature-less
3815
+ * blocks. Persisting those would corrupt the next turn. Reasoning from
3816
+ * already-completed prior steps lives on STEP_FINISHED-flushed assistant
3817
+ * messages and is untouched. Aborted-step messages therefore never carry
3818
+ * reasoningParts.
3819
+ */
3820
+ finalizeAbortedRun() {
3821
+ this._currentReasoningBlocks = [];
3822
+ this._currentReasoningBlockText = "";
3823
+ if (!this._currentAssistantMessage) return;
3824
+ if (this._currentAssistantToolCalls.length > 0) {
3825
+ this._messages.push({
3826
+ id: this._currentAssistantMessage.id || uuidv42(),
3827
+ role: "assistant",
3828
+ content: this._currentMessageContent || "",
3829
+ toolCalls: [...this._currentAssistantToolCalls]
3830
+ });
3831
+ this._messages.push(...this._pendingToolResults);
3832
+ const respondedIds = new Set(
3833
+ this._pendingToolResults.map((m) => "toolCallId" in m ? m.toolCallId : void 0).filter((id) => typeof id === "string")
3834
+ );
3835
+ for (const tc of this._currentAssistantToolCalls) {
3836
+ if (!respondedIds.has(tc.id)) {
3837
+ this._messages.push({
3838
+ id: uuidv42(),
3839
+ role: "tool",
3840
+ content: JSON.stringify({ aborted: true, reason: "Cancelled by user before tool finished" }),
3841
+ toolCallId: tc.id
3842
+ });
3843
+ }
3844
+ }
3845
+ this._currentMessageContent = "";
3846
+ } else if (this._currentMessageContent) {
3847
+ this._messages.push({
3848
+ id: this._currentAssistantMessage.id || uuidv42(),
3849
+ role: "assistant",
3850
+ content: this._currentMessageContent
3851
+ });
3852
+ }
3853
+ this._currentAssistantMessage = null;
3854
+ this._currentAssistantToolCalls = [];
3855
+ this._pendingToolResults = [];
3856
+ }
3699
3857
  /**
3700
3858
  * Sends a tool approval response back to the server.
3701
3859
  *
@@ -3790,6 +3948,12 @@ var UseAIClient = class {
3790
3948
  get currentMessageContent() {
3791
3949
  return this._currentMessageContent;
3792
3950
  }
3951
+ /**
3952
+ * Gets the runId of the in-flight run, or null when no run is active.
3953
+ */
3954
+ get currentRunId() {
3955
+ return this._currentRunId;
3956
+ }
3793
3957
  /**
3794
3958
  * Gets the current reasoning blocks collected during the current run.
3795
3959
  */
@@ -4255,7 +4419,7 @@ import { useState as useState7, useCallback as useCallback5, useRef as useRef5,
4255
4419
 
4256
4420
  // src/utils/messageConversion.ts
4257
4421
  function transformMessagesToClientFormat(persistedMessages) {
4258
- return persistedMessages.map((msg) => {
4422
+ return persistedMessages.filter((msg) => msg.displayMode !== "info").map((msg) => {
4259
4423
  switch (msg.role) {
4260
4424
  case "tool":
4261
4425
  return {
@@ -5233,6 +5397,7 @@ function useServerEvents({
5233
5397
  const loadingRef = useRef10(loading);
5234
5398
  loadingRef.current = loading;
5235
5399
  const messageCountAtRunStartRef = useRef10(0);
5400
+ const runIdAtRunStartRef = useRef10(void 0);
5236
5401
  const hasTextFromPriorStepRef = useRef10(false);
5237
5402
  const [executingToolRaw, setExecutingTool] = useState13(null);
5238
5403
  const executingToolFallbackRef = useRef10(null);
@@ -5245,11 +5410,37 @@ function useServerEvents({
5245
5410
  saveAIResponseRef.current = saveAIResponse;
5246
5411
  const stringsRef = useRef10(strings);
5247
5412
  stringsRef.current = strings;
5413
+ const persistFinalResponse = useCallback11(async (client, opts) => {
5414
+ const content = client.currentMessageContent;
5415
+ const reasoningParts = client.currentReasoningBlocks.length > 0 ? [...client.currentReasoningBlocks] : void 0;
5416
+ const turnMessages = extractTurnMessages(client.messages, messageCountAtRunStartRef.current);
5417
+ if (opts.aborted) {
5418
+ const notice = stringsRef.current.notices.aborted;
5419
+ if (content) {
5420
+ await saveAIResponseRef.current(content, void 0, opts.traceId, turnMessages, reasoningParts);
5421
+ await saveAIResponseRef.current(notice, "info");
5422
+ } else {
5423
+ await saveAIResponseRef.current(notice, "info", opts.traceId, turnMessages);
5424
+ }
5425
+ return;
5426
+ }
5427
+ if (content) {
5428
+ await saveAIResponseRef.current(content, void 0, opts.traceId, turnMessages, reasoningParts);
5429
+ }
5430
+ }, []);
5431
+ const resetRunUiState = useCallback11(() => {
5432
+ setStreamingText("");
5433
+ setStreamingReasoning("");
5434
+ streamingChatIdRef.current = null;
5435
+ setExecutingTool(null);
5436
+ setLoading(false);
5437
+ }, []);
5248
5438
  const handleServerEvent = useCallback11(async (client, event) => {
5249
5439
  const ts = toolSystemRef.current;
5250
5440
  const strs = stringsRef.current;
5251
5441
  if (event.type === EventType2.RUN_STARTED) {
5252
5442
  messageCountAtRunStartRef.current = client.messages.length;
5443
+ runIdAtRunStartRef.current = client.currentRunId ?? void 0;
5253
5444
  hasTextFromPriorStepRef.current = false;
5254
5445
  setStreamingReasoning("");
5255
5446
  } else if (event.type === EventType2.REASONING_MESSAGE_START) {
@@ -5301,30 +5492,21 @@ function useServerEvents({
5301
5492
  setStreamingText((prev) => prev + contentEvent.delta);
5302
5493
  } else if (event.type === EventType2.TEXT_MESSAGE_END) {
5303
5494
  } else if (event.type === EventType2.RUN_FINISHED) {
5304
- const content = client.currentMessageContent;
5305
- if (content) {
5306
- const finishedEvent = event;
5307
- const traceId = finishedEvent.runId;
5308
- const turnMessages = extractTurnMessages(client.messages, messageCountAtRunStartRef.current);
5309
- const reasoningParts = client.currentReasoningBlocks.length > 0 ? [...client.currentReasoningBlocks] : void 0;
5310
- saveAIResponseRef.current(content, void 0, traceId, turnMessages, reasoningParts);
5311
- }
5312
- setStreamingText("");
5313
- setStreamingReasoning("");
5314
- streamingChatIdRef.current = null;
5315
- setExecutingTool(null);
5316
- setLoading(false);
5495
+ const finishedEvent = event;
5496
+ await persistFinalResponse(client, { aborted: false, traceId: finishedEvent.runId });
5497
+ resetRunUiState();
5317
5498
  } else if (event.type === EventType2.RUN_ERROR) {
5318
5499
  const errorEvent = event;
5319
5500
  const errorCode = errorEvent.message;
5320
- console.error("[ServerEvents] Run error:", errorCode);
5321
- const userMessage = strs.errors[errorCode] || errorEvent.message || strs.errors[ErrorCode.UNKNOWN_ERROR];
5322
- saveAIResponseRef.current(userMessage, "error");
5323
- setStreamingText("");
5324
- setStreamingReasoning("");
5325
- streamingChatIdRef.current = null;
5326
- setExecutingTool(null);
5327
- setLoading(false);
5501
+ if (errorCode === ErrorCode.ABORTED) {
5502
+ client.finalizeRun({ aborted: true });
5503
+ await persistFinalResponse(client, { aborted: true, traceId: runIdAtRunStartRef.current });
5504
+ } else {
5505
+ console.error("[ServerEvents] Run error:", errorCode);
5506
+ const userMessage = strs.errors[errorCode] || errorEvent.message || strs.errors[ErrorCode.UNKNOWN_ERROR];
5507
+ saveAIResponseRef.current(userMessage, "error");
5508
+ }
5509
+ resetRunUiState();
5328
5510
  }
5329
5511
  }, []);
5330
5512
  const handleDisconnect = useCallback11(() => {
@@ -5494,6 +5676,8 @@ var noOpContextValue = {
5494
5676
  },
5495
5677
  delete: async () => {
5496
5678
  }
5679
+ },
5680
+ abortRun: () => {
5497
5681
  }
5498
5682
  };
5499
5683
  var DEFAULT_FILE_UPLOAD_CONFIG = {
@@ -5627,6 +5811,9 @@ function UseAIProvider({
5627
5811
  console.error("Failed to register tools:", err);
5628
5812
  }
5629
5813
  }, [toolSystem.hasTools, toolSystem.aggregatedTools, connected]);
5814
+ const abortRun = useCallback13(() => {
5815
+ clientRef.current?.abortRun();
5816
+ }, []);
5630
5817
  const handleSendMessage = useCallback13(async (message, attachments, messageForwardedProps) => {
5631
5818
  if (!clientRef.current) return;
5632
5819
  serverEvents.clearStreamingText();
@@ -5725,7 +5912,8 @@ function UseAIProvider({
5725
5912
  save: saveCommand,
5726
5913
  rename: renameCommand,
5727
5914
  delete: deleteCommand
5728
- }
5915
+ },
5916
+ abortRun
5729
5917
  };
5730
5918
  const effectiveStreamingText = serverEvents.streamingChatIdRef.current === chatManagement.displayedChatId ? serverEvents.streamingText : "";
5731
5919
  const effectiveStreamingReasoning = serverEvents.streamingChatIdRef.current === chatManagement.displayedChatId ? serverEvents.streamingReasoning : "";
@@ -5733,6 +5921,7 @@ function UseAIProvider({
5733
5921
  connected,
5734
5922
  loading: serverEvents.loading,
5735
5923
  sendMessage: handleSendMessage,
5924
+ abortRun,
5736
5925
  messages,
5737
5926
  streamingText: effectiveStreamingText,
5738
5927
  streamingReasoning: effectiveStreamingReasoning,
@@ -5782,6 +5971,7 @@ function UseAIProvider({
5782
5971
  const hasCustomChat = CustomChat !== void 0 && CustomChat !== null;
5783
5972
  const chatPanelProps = {
5784
5973
  onSendMessage: handleSendMessage,
5974
+ onAbort: abortRun,
5785
5975
  messages,
5786
5976
  loading: serverEvents.loading,
5787
5977
  connected,
@@ -5829,6 +6019,7 @@ function UseAIProvider({
5829
6019
  isOpen: isChatOpen,
5830
6020
  onClose: () => handleSetChatOpen(false),
5831
6021
  onSendMessage: handleSendMessage,
6022
+ onAbort: abortRun,
5832
6023
  messages,
5833
6024
  loading: serverEvents.loading,
5834
6025
  connected,