@meetsmore-oss/use-ai-client 1.13.0 → 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,37 +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
- }
3621
+ this.finalizeRun({ aborted: false });
3622
+ }
3623
+ if (event.type === EventType.RUN_FINISHED || event.type === EventType.RUN_ERROR) {
3624
+ this._currentRunId = null;
3577
3625
  }
3578
3626
  this.eventHandlers.forEach((handler) => handler(event));
3579
3627
  }
@@ -3637,10 +3685,12 @@ var UseAIClient = class {
3637
3685
  // Type cast needed for Message type compatibility
3638
3686
  };
3639
3687
  this._messages.push(userMessage);
3688
+ const runId = uuidv42();
3689
+ this._currentRunId = runId;
3640
3690
  const runInput = {
3641
3691
  threadId: this.threadId,
3642
3692
  // Use getter to ensure non-null
3643
- runId: uuidv42(),
3693
+ runId,
3644
3694
  messages: this._messages,
3645
3695
  tools: this._tools.map((t) => ({
3646
3696
  name: t.name,
@@ -3695,6 +3745,115 @@ var UseAIClient = class {
3695
3745
  this._pendingToolResults.push(toolResultMsg);
3696
3746
  this.send(toolResultMessage);
3697
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
+ }
3698
3857
  /**
3699
3858
  * Sends a tool approval response back to the server.
3700
3859
  *
@@ -3789,6 +3948,12 @@ var UseAIClient = class {
3789
3948
  get currentMessageContent() {
3790
3949
  return this._currentMessageContent;
3791
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
+ }
3792
3957
  /**
3793
3958
  * Gets the current reasoning blocks collected during the current run.
3794
3959
  */
@@ -4254,7 +4419,7 @@ import { useState as useState7, useCallback as useCallback5, useRef as useRef5,
4254
4419
 
4255
4420
  // src/utils/messageConversion.ts
4256
4421
  function transformMessagesToClientFormat(persistedMessages) {
4257
- return persistedMessages.map((msg) => {
4422
+ return persistedMessages.filter((msg) => msg.displayMode !== "info").map((msg) => {
4258
4423
  switch (msg.role) {
4259
4424
  case "tool":
4260
4425
  return {
@@ -5232,6 +5397,7 @@ function useServerEvents({
5232
5397
  const loadingRef = useRef10(loading);
5233
5398
  loadingRef.current = loading;
5234
5399
  const messageCountAtRunStartRef = useRef10(0);
5400
+ const runIdAtRunStartRef = useRef10(void 0);
5235
5401
  const hasTextFromPriorStepRef = useRef10(false);
5236
5402
  const [executingToolRaw, setExecutingTool] = useState13(null);
5237
5403
  const executingToolFallbackRef = useRef10(null);
@@ -5244,11 +5410,37 @@ function useServerEvents({
5244
5410
  saveAIResponseRef.current = saveAIResponse;
5245
5411
  const stringsRef = useRef10(strings);
5246
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
+ }, []);
5247
5438
  const handleServerEvent = useCallback11(async (client, event) => {
5248
5439
  const ts = toolSystemRef.current;
5249
5440
  const strs = stringsRef.current;
5250
5441
  if (event.type === EventType2.RUN_STARTED) {
5251
5442
  messageCountAtRunStartRef.current = client.messages.length;
5443
+ runIdAtRunStartRef.current = client.currentRunId ?? void 0;
5252
5444
  hasTextFromPriorStepRef.current = false;
5253
5445
  setStreamingReasoning("");
5254
5446
  } else if (event.type === EventType2.REASONING_MESSAGE_START) {
@@ -5300,30 +5492,21 @@ function useServerEvents({
5300
5492
  setStreamingText((prev) => prev + contentEvent.delta);
5301
5493
  } else if (event.type === EventType2.TEXT_MESSAGE_END) {
5302
5494
  } else if (event.type === EventType2.RUN_FINISHED) {
5303
- const content = client.currentMessageContent;
5304
- if (content) {
5305
- const finishedEvent = event;
5306
- const traceId = finishedEvent.runId;
5307
- const turnMessages = extractTurnMessages(client.messages, messageCountAtRunStartRef.current);
5308
- const reasoningParts = client.currentReasoningBlocks.length > 0 ? [...client.currentReasoningBlocks] : void 0;
5309
- saveAIResponseRef.current(content, void 0, traceId, turnMessages, reasoningParts);
5310
- }
5311
- setStreamingText("");
5312
- setStreamingReasoning("");
5313
- streamingChatIdRef.current = null;
5314
- setExecutingTool(null);
5315
- setLoading(false);
5495
+ const finishedEvent = event;
5496
+ await persistFinalResponse(client, { aborted: false, traceId: finishedEvent.runId });
5497
+ resetRunUiState();
5316
5498
  } else if (event.type === EventType2.RUN_ERROR) {
5317
5499
  const errorEvent = event;
5318
5500
  const errorCode = errorEvent.message;
5319
- console.error("[ServerEvents] Run error:", errorCode);
5320
- const userMessage = strs.errors[errorCode] || errorEvent.message || strs.errors[ErrorCode.UNKNOWN_ERROR];
5321
- saveAIResponseRef.current(userMessage, "error");
5322
- setStreamingText("");
5323
- setStreamingReasoning("");
5324
- streamingChatIdRef.current = null;
5325
- setExecutingTool(null);
5326
- 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();
5327
5510
  }
5328
5511
  }, []);
5329
5512
  const handleDisconnect = useCallback11(() => {
@@ -5493,6 +5676,8 @@ var noOpContextValue = {
5493
5676
  },
5494
5677
  delete: async () => {
5495
5678
  }
5679
+ },
5680
+ abortRun: () => {
5496
5681
  }
5497
5682
  };
5498
5683
  var DEFAULT_FILE_UPLOAD_CONFIG = {
@@ -5626,6 +5811,9 @@ function UseAIProvider({
5626
5811
  console.error("Failed to register tools:", err);
5627
5812
  }
5628
5813
  }, [toolSystem.hasTools, toolSystem.aggregatedTools, connected]);
5814
+ const abortRun = useCallback13(() => {
5815
+ clientRef.current?.abortRun();
5816
+ }, []);
5629
5817
  const handleSendMessage = useCallback13(async (message, attachments, messageForwardedProps) => {
5630
5818
  if (!clientRef.current) return;
5631
5819
  serverEvents.clearStreamingText();
@@ -5724,7 +5912,8 @@ function UseAIProvider({
5724
5912
  save: saveCommand,
5725
5913
  rename: renameCommand,
5726
5914
  delete: deleteCommand
5727
- }
5915
+ },
5916
+ abortRun
5728
5917
  };
5729
5918
  const effectiveStreamingText = serverEvents.streamingChatIdRef.current === chatManagement.displayedChatId ? serverEvents.streamingText : "";
5730
5919
  const effectiveStreamingReasoning = serverEvents.streamingChatIdRef.current === chatManagement.displayedChatId ? serverEvents.streamingReasoning : "";
@@ -5732,6 +5921,7 @@ function UseAIProvider({
5732
5921
  connected,
5733
5922
  loading: serverEvents.loading,
5734
5923
  sendMessage: handleSendMessage,
5924
+ abortRun,
5735
5925
  messages,
5736
5926
  streamingText: effectiveStreamingText,
5737
5927
  streamingReasoning: effectiveStreamingReasoning,
@@ -5781,6 +5971,7 @@ function UseAIProvider({
5781
5971
  const hasCustomChat = CustomChat !== void 0 && CustomChat !== null;
5782
5972
  const chatPanelProps = {
5783
5973
  onSendMessage: handleSendMessage,
5974
+ onAbort: abortRun,
5784
5975
  messages,
5785
5976
  loading: serverEvents.loading,
5786
5977
  connected,
@@ -5828,6 +6019,7 @@ function UseAIProvider({
5828
6019
  isOpen: isChatOpen,
5829
6020
  onClose: () => handleSetChatOpen(false),
5830
6021
  onSendMessage: handleSendMessage,
6022
+ onAbort: abortRun,
5831
6023
  messages,
5832
6024
  loading: serverEvents.loading,
5833
6025
  connected,