@hef2024/llmasaservice-ui 0.24.4 → 0.24.6

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.
@@ -66,6 +66,33 @@ export type LocalToolExecutor = (
66
66
  context: LocalToolExecutorContext,
67
67
  ) => Promise<unknown> | unknown;
68
68
 
69
+ export type ArtifactBlockType = 'thinking' | 'reasoning' | 'searching' | 'planning';
70
+ export type PlanningStepStatus = 'todo' | 'doing' | 'done';
71
+
72
+ export interface PlanningStep {
73
+ status: PlanningStepStatus;
74
+ title: string;
75
+ }
76
+
77
+ export interface ResponseArtifactBlock {
78
+ type: ArtifactBlockType;
79
+ content: string;
80
+ index: number;
81
+ signature: string;
82
+ steps?: PlanningStep[];
83
+ }
84
+
85
+ interface HistoryEntry {
86
+ content: string;
87
+ callId: string;
88
+ toolCalls?: any[];
89
+ toolResponses?: any[];
90
+ artifactBlocks?: ResponseArtifactBlock[];
91
+ planningBlocks?: ResponseArtifactBlock[];
92
+ }
93
+
94
+ export type AIChatHistoryEntry = HistoryEntry;
95
+
69
96
  export interface AIChatPanelProps {
70
97
  project_id: string;
71
98
  initialPrompt?: string;
@@ -80,7 +107,7 @@ export interface AIChatPanelProps {
80
107
  theme?: 'light' | 'dark';
81
108
  url?: string | null;
82
109
  service?: string | null;
83
- historyChangedCallback?: (history: Record<string, { content: string; callId: string }>) => void;
110
+ historyChangedCallback?: (history: Record<string, AIChatHistoryEntry>) => void;
84
111
  responseCompleteCallback?: (callId: string, prompt: string, response: string) => void;
85
112
  onLoadingChange?: (isLoading: boolean) => void;
86
113
  promptTemplate?: string;
@@ -99,7 +126,7 @@ export interface AIChatPanelProps {
99
126
  showPoweredBy?: boolean;
100
127
  agent?: string | null;
101
128
  conversation?: string | null;
102
- initialHistory?: Record<string, { content: string; callId: string }>;
129
+ initialHistory?: Record<string, AIChatHistoryEntry>;
103
130
  hideRagContextInPrompt?: boolean;
104
131
  createConversationOnFirstChat?: boolean;
105
132
  autoApproveTools?: boolean | string[];
@@ -167,19 +194,7 @@ export interface ContextSection {
167
194
 
168
195
  export type ContextDataFormat = 'json' | 'toon' | 'markdown' | 'text';
169
196
 
170
- interface HistoryEntry {
171
- content: string;
172
- callId: string;
173
- toolCalls?: any[];
174
- toolResponses?: any[];
175
- }
176
-
177
- interface ThinkingBlock {
178
- type: 'thinking' | 'reasoning' | 'searching';
179
- content: string;
180
- index: number;
181
- signature: string;
182
- }
197
+ type ThinkingBlock = ResponseArtifactBlock;
183
198
 
184
199
  interface ToolRequestMatch {
185
200
  match: string;
@@ -215,7 +230,7 @@ interface InlineToolMarker {
215
230
  }
216
231
 
217
232
  interface InlineThinkingMarker {
218
- type: 'thinking' | 'reasoning' | 'searching';
233
+ type: ArtifactBlockType;
219
234
  signature: string;
220
235
  }
221
236
 
@@ -1009,7 +1024,7 @@ const hashInlineMarkerValue = (value: string): string => {
1009
1024
  };
1010
1025
 
1011
1026
  const getThinkingBlockSignature = (
1012
- type: 'thinking' | 'reasoning' | 'searching',
1027
+ type: ArtifactBlockType,
1013
1028
  content: string,
1014
1029
  ): string => {
1015
1030
  const normalizedContent = String(content || '').trim();
@@ -1017,16 +1032,112 @@ const getThinkingBlockSignature = (
1017
1032
  };
1018
1033
 
1019
1034
  const buildThinkingBlockMarker = (
1020
- type: 'thinking' | 'reasoning' | 'searching',
1035
+ type: ArtifactBlockType,
1021
1036
  signature: string,
1022
1037
  ): string => {
1023
- const normalizedType = String(type || 'thinking').trim() as 'thinking' | 'reasoning' | 'searching';
1038
+ const normalizedType = String(type || 'thinking').trim() as ArtifactBlockType;
1024
1039
  const normalizedSignature = String(signature || '').trim() || `${normalizedType}-block`;
1025
1040
  return `${INLINE_THINKING_MARKER_PREFIX}${encodeURIComponent(normalizedType)}|${encodeURIComponent(
1026
1041
  normalizedSignature,
1027
1042
  )}${INLINE_THINKING_MARKER_SUFFIX}`;
1028
1043
  };
1029
1044
 
1045
+ const PLAN_STEP_REGEX = /^\s*\[plan-step\]\s*([a-zA-Z_-]+)\s*:\s*(.+?)\s*$/i;
1046
+
1047
+ const normalizePlanningStatus = (value: string): PlanningStepStatus => {
1048
+ const normalized = String(value || '').trim().toLowerCase();
1049
+ if (normalized === 'doing' || normalized === 'in_progress' || normalized === 'in-progress') {
1050
+ return 'doing';
1051
+ }
1052
+ if (normalized === 'done' || normalized === 'completed') {
1053
+ return 'done';
1054
+ }
1055
+ return 'todo';
1056
+ };
1057
+
1058
+ const formatPlanningStatus = (status: PlanningStepStatus): string => {
1059
+ if (status === 'doing') return 'Doing';
1060
+ if (status === 'done') return 'Done';
1061
+ return 'Todo';
1062
+ };
1063
+
1064
+ const parsePlanningStep = (line: string): PlanningStep | null => {
1065
+ const match = PLAN_STEP_REGEX.exec(String(line || ''));
1066
+ if (!match) return null;
1067
+
1068
+ const title = String(match[2] || '').trim();
1069
+ if (!title) return null;
1070
+
1071
+ return {
1072
+ status: normalizePlanningStatus(match[1] || ''),
1073
+ title,
1074
+ };
1075
+ };
1076
+
1077
+ const buildPlanningBlockContent = (steps: PlanningStep[]): string => {
1078
+ return steps
1079
+ .filter((step) => !!step && typeof step.title === 'string' && step.title.trim().length > 0)
1080
+ .map((step) => `${formatPlanningStatus(step.status)}: ${step.title.trim()}`)
1081
+ .join('\n');
1082
+ };
1083
+
1084
+ const extractPlanningBlocks = (
1085
+ text: string,
1086
+ ): { textWithMarkers: string; planningBlocks: ResponseArtifactBlock[] } => {
1087
+ const source = typeof text === 'string' ? text : '';
1088
+ if (!source) {
1089
+ return {
1090
+ textWithMarkers: '',
1091
+ planningBlocks: [],
1092
+ };
1093
+ }
1094
+
1095
+ const lines = source.split('\n');
1096
+ const planningBlocks: ResponseArtifactBlock[] = [];
1097
+ const rebuiltSegments: string[] = [];
1098
+ let currentOffset = 0;
1099
+ let pendingSteps: PlanningStep[] = [];
1100
+ let pendingIndex = 0;
1101
+
1102
+ const flushPendingPlanning = () => {
1103
+ if (pendingSteps.length === 0) return;
1104
+
1105
+ const signature = `planning-${pendingIndex}`;
1106
+ planningBlocks.push({
1107
+ type: 'planning',
1108
+ content: buildPlanningBlockContent(pendingSteps),
1109
+ index: pendingIndex,
1110
+ signature,
1111
+ steps: pendingSteps,
1112
+ });
1113
+ rebuiltSegments.push(`\n\n${buildThinkingBlockMarker('planning', signature)}\n\n`);
1114
+ pendingSteps = [];
1115
+ };
1116
+
1117
+ lines.forEach((line, index) => {
1118
+ const lineWithNewline = index < lines.length - 1 ? `${line}\n` : line;
1119
+ const planningStep = parsePlanningStep(line);
1120
+ if (planningStep) {
1121
+ if (pendingSteps.length === 0) {
1122
+ pendingIndex = currentOffset;
1123
+ }
1124
+ pendingSteps = [...pendingSteps, planningStep];
1125
+ } else {
1126
+ flushPendingPlanning();
1127
+ rebuiltSegments.push(lineWithNewline);
1128
+ }
1129
+
1130
+ currentOffset += lineWithNewline.length;
1131
+ });
1132
+
1133
+ flushPendingPlanning();
1134
+
1135
+ return {
1136
+ textWithMarkers: rebuiltSegments.join(''),
1137
+ planningBlocks,
1138
+ };
1139
+ };
1140
+
1030
1141
  const buildInlineToolMarker = (toolName: string, callId: string): string => {
1031
1142
  const normalizedToolName = String(toolName || '').trim() || 'tool';
1032
1143
  const normalizedCallId = String(callId || '').trim() || `${normalizedToolName}-call`;
@@ -1110,11 +1221,13 @@ const parseInlineThinkingMarkers = (
1110
1221
  }
1111
1222
 
1112
1223
  const normalizedType = String(rawType || '').trim().toLowerCase();
1113
- const type: 'thinking' | 'reasoning' | 'searching' =
1224
+ const type: ArtifactBlockType =
1114
1225
  normalizedType === 'reasoning'
1115
1226
  ? 'reasoning'
1116
1227
  : normalizedType === 'searching'
1117
1228
  ? 'searching'
1229
+ : normalizedType === 'planning'
1230
+ ? 'planning'
1118
1231
  : 'thinking';
1119
1232
 
1120
1233
  markers.push({
@@ -1792,6 +1905,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
1792
1905
  const [lastPrompt, setLastPrompt] = useState<string | null>(null);
1793
1906
  const [lastKey, setLastKey] = useState<string | null>(null);
1794
1907
  const [currentConversation, setCurrentConversation] = useState<string | null>(conversation);
1908
+ const ensuredConversationIdsRef = useRef<Set<string>>(new Set());
1795
1909
  const [followOnQuestionsState, setFollowOnQuestionsState] = useState(followOnQuestions);
1796
1910
  const [thinkingBlocks, setThinkingBlocks] = useState<ThinkingBlock[]>([]);
1797
1911
  const [currentThinkingIndex, setCurrentThinkingIndex] = useState(0);
@@ -2006,7 +2120,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
2006
2120
  }
2007
2121
 
2008
2122
  try {
2009
- const enriched = await Promise.all(
2123
+ const settled = await Promise.allSettled(
2010
2124
  mcpServers.map(async (server) => {
2011
2125
  const resolved = await resolveMcpAuthHeaders({
2012
2126
  phase: 'list',
@@ -2025,6 +2139,23 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
2025
2139
  };
2026
2140
  })
2027
2141
  );
2142
+
2143
+ const enriched = settled.map((result, index) => {
2144
+ if (result.status === 'fulfilled') {
2145
+ return result.value;
2146
+ }
2147
+
2148
+ const failingUrl =
2149
+ typeof mcpServers?.[index]?.url === 'string'
2150
+ ? mcpServers[index].url
2151
+ : `mcp[${index}]`;
2152
+ console.error(
2153
+ `[AIChatPanel] Failed to resolve MCP auth headers for ${failingUrl}:`,
2154
+ result.reason
2155
+ );
2156
+ return mcpServers[index];
2157
+ });
2158
+
2028
2159
  if (!cancelled) setResolvedMcpServers(enriched);
2029
2160
  } catch (error) {
2030
2161
  console.error('[AIChatPanel] Failed to resolve MCP auth headers:', error);
@@ -2142,10 +2273,26 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
2142
2273
  }));
2143
2274
  });
2144
2275
 
2145
- const results = await Promise.all(fetchPromises);
2146
- const allTools = results.flat();
2276
+ const settled = await Promise.allSettled(fetchPromises);
2277
+ const allTools = settled.flatMap((result, index) => {
2278
+ if (result.status === 'fulfilled') {
2279
+ return result.value;
2280
+ }
2281
+
2282
+ const failingUrl =
2283
+ typeof resolvedMcpServers?.[index]?.url === 'string'
2284
+ ? resolvedMcpServers[index].url
2285
+ : `mcp[${index}]`;
2286
+ console.error(
2287
+ `[AIChatPanel] Failed to load MCP tools from ${failingUrl}:`,
2288
+ result.reason
2289
+ );
2290
+ return [];
2291
+ });
2292
+
2293
+ const failedCount = settled.filter((result) => result.status === 'rejected').length;
2147
2294
  setToolList(allTools);
2148
- setToolsFetchError(false);
2295
+ setToolsFetchError(failedCount > 0 && allTools.length === 0);
2149
2296
  } catch (error) {
2150
2297
  console.error('[AIChatPanel] Failed to load MCP tools:', error);
2151
2298
  setToolList([]);
@@ -2238,28 +2385,36 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
2238
2385
  const ensureConversation = useCallback(() => {
2239
2386
  const normalizedConversationId =
2240
2387
  typeof currentConversation === 'string' ? currentConversation.trim() : '';
2388
+ const hasConversationHistory = Object.keys(latestHistoryRef.current || {}).length > 0;
2389
+ const shouldInitializeProvidedConversation =
2390
+ normalizedConversationId.length > 10 &&
2391
+ createConversationOnFirstChat &&
2392
+ !hasConversationHistory &&
2393
+ !ensuredConversationIdsRef.current.has(normalizedConversationId);
2241
2394
 
2242
2395
  console.log('ensureConversation - called with:', {
2243
2396
  currentConversation: normalizedConversationId || null,
2244
2397
  createConversationOnFirstChat,
2398
+ shouldInitializeProvidedConversation,
2245
2399
  project_id,
2246
2400
  publicAPIUrl,
2247
2401
  });
2248
2402
 
2249
- // Existing conversation supplied by caller: use it directly.
2250
- if (normalizedConversationId) {
2403
+ // Existing conversation supplied by caller: initialize its record once if needed,
2404
+ // otherwise use it directly.
2405
+ if (normalizedConversationId && !shouldInitializeProvidedConversation) {
2251
2406
  console.log('ensureConversation - using existing conversation:', normalizedConversationId);
2252
2407
  return Promise.resolve(normalizedConversationId);
2253
2408
  }
2254
2409
 
2255
2410
  if (!createConversationOnFirstChat) {
2256
- return Promise.resolve('');
2411
+ return Promise.resolve(normalizedConversationId || '');
2257
2412
  }
2258
2413
 
2259
2414
  // Guard: Don't create/ensure conversations without a project_id
2260
2415
  if (!project_id) {
2261
2416
  console.error('ensureConversation - Cannot create conversation without project_id');
2262
- return Promise.resolve('');
2417
+ return Promise.resolve(normalizedConversationId || '');
2263
2418
  }
2264
2419
 
2265
2420
  const createConversation = () => {
@@ -2272,6 +2427,10 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
2272
2427
  language: browserInfo?.userLanguage,
2273
2428
  };
2274
2429
 
2430
+ if (shouldInitializeProvidedConversation) {
2431
+ requestBody.conversationId = normalizedConversationId;
2432
+ }
2433
+
2275
2434
  console.log('ensureConversation - Creating conversation with:', requestBody);
2276
2435
  console.log('ensureConversation - API URL:', `${publicAPIUrl}/conversations`);
2277
2436
 
@@ -2299,21 +2458,40 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
2299
2458
  (typeof newConvo?.conversation_id === 'string' && newConvo.conversation_id.trim()) ||
2300
2459
  (typeof newConvo?.conversation?.id === 'string' && newConvo.conversation.id.trim()) ||
2301
2460
  '';
2461
+ const resolvedConversationId = normalizedConversationId || createdId;
2302
2462
 
2303
2463
  if (createdId) {
2304
2464
  console.log('ensureConversation - New conversation ID:', createdId);
2465
+ }
2466
+
2467
+ if (resolvedConversationId) {
2468
+ ensuredConversationIdsRef.current.add(resolvedConversationId);
2469
+ }
2470
+
2471
+ if (normalizedConversationId && createdId && createdId !== normalizedConversationId) {
2472
+ console.warn(
2473
+ 'ensureConversation - API returned a different ID than supplied conversationId; keeping caller-provided ID',
2474
+ { suppliedConversationId: normalizedConversationId, returnedConversationId: createdId }
2475
+ );
2476
+ }
2477
+
2478
+ if (!normalizedConversationId && createdId) {
2305
2479
  setCurrentConversation(createdId);
2306
2480
  // NOTE: Don't call onConversationCreated here - it causes a re-render
2307
2481
  // before send() is called. The caller should notify after send() starts.
2308
2482
  return createdId;
2309
2483
  }
2310
2484
 
2485
+ if (resolvedConversationId) {
2486
+ return resolvedConversationId;
2487
+ }
2488
+
2311
2489
  console.warn('ensureConversation - No ID in response');
2312
- return '';
2490
+ return normalizedConversationId || '';
2313
2491
  })
2314
2492
  .catch((error) => {
2315
2493
  console.error('Error creating new conversation', error);
2316
- return '';
2494
+ return normalizedConversationId || '';
2317
2495
  });
2318
2496
  };
2319
2497
 
@@ -3267,7 +3445,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
3267
3445
  return cleaned || 'Thinking';
3268
3446
  }, []);
3269
3447
 
3270
- // Process thinking tags from response
3448
+ // Process internal response artifacts from the model before rendering transcript text.
3271
3449
  const processThinkingTags = useCallback((text: string): {
3272
3450
  cleanedText: string;
3273
3451
  completedBlocks: ThinkingBlock[];
@@ -3283,7 +3461,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
3283
3461
  };
3284
3462
  }
3285
3463
 
3286
- // Remove zero-width space characters from keepalive before processing
3464
+ // Remove zero-width space characters from keepalive before processing.
3287
3465
  const processedText = text.replace(/\u200B/g, '');
3288
3466
 
3289
3467
  const completedBlocks: ThinkingBlock[] = [];
@@ -3291,7 +3469,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
3291
3469
  /<(thinking|reasoning|searching)>([\s\S]*?)<\/\1>/gi,
3292
3470
  (_fullMatch, rawType, rawContent, offset) => {
3293
3471
  const normalizedType = String(rawType || '').trim().toLowerCase();
3294
- const type: 'thinking' | 'reasoning' | 'searching' =
3472
+ const type: ArtifactBlockType =
3295
3473
  normalizedType === 'reasoning'
3296
3474
  ? 'reasoning'
3297
3475
  : normalizedType === 'searching'
@@ -3311,6 +3489,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
3311
3489
  return `\n\n${buildThinkingBlockMarker(type, signature)}\n\n`;
3312
3490
  },
3313
3491
  );
3492
+ const {
3493
+ textWithMarkers: textWithArtifactMarkers,
3494
+ planningBlocks,
3495
+ } = extractPlanningBlocks(textWithCompleteMarkers);
3496
+ if (planningBlocks.length > 0) {
3497
+ completedBlocks.push(...planningBlocks);
3498
+ }
3314
3499
 
3315
3500
  // Check for incomplete (streaming) tags at the end of the text
3316
3501
  let activeBlock: { type: 'thinking' | 'reasoning' | 'searching'; content: string } | null = null;
@@ -3353,8 +3538,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
3353
3538
  }
3354
3539
  }
3355
3540
 
3356
- // Clean the text by removing all thinking-related tags (complete and incomplete)
3357
- let cleanedText = textWithCompleteMarkers
3541
+ // Clean the text by removing all thinking-related tags (complete and incomplete).
3542
+ let cleanedText = textWithArtifactMarkers
3358
3543
  // Also remove partial opening tags
3359
3544
  .replace(/<think(?:i(?:n(?:g)?)?)?$/i, '')
3360
3545
  .replace(/<reas(?:o(?:n(?:i(?:n(?:g)?)?)?)?)?$/i, '')
@@ -3395,6 +3580,16 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
3395
3580
  return incoming;
3396
3581
  }
3397
3582
 
3583
+ const incomingSignaturePrefix =
3584
+ incoming.length >= existing.length &&
3585
+ existing.every((block, index) => {
3586
+ const next = incoming[index];
3587
+ return !!next && next.type === block.type && next.signature === block.signature;
3588
+ });
3589
+ if (incomingSignaturePrefix) {
3590
+ return incoming;
3591
+ }
3592
+
3398
3593
  // For follow-on streams (e.g., tool continuations), append only unseen blocks.
3399
3594
  const merged = [...existing];
3400
3595
  const seen = new Set(existing.map((block) => block.signature || `${block.type}::${block.content}`));
@@ -3705,7 +3900,6 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
3705
3900
 
3706
3901
  // Clear any previous errors
3707
3902
  setError(null);
3708
- lastProcessedErrorRef.current = null; // Allow new errors to be processed
3709
3903
 
3710
3904
  // Reset scroll tracking for new message - enable auto-scroll
3711
3905
  setUserHasScrolled(false);
@@ -4100,6 +4294,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
4100
4294
 
4101
4295
  // Keep reasoning blocks visible; users can collapse manually if needed.
4102
4296
  // Auto-collapse here made blocks appear and then seem to disappear.
4297
+ const planningBlocks = mergedBlocks.filter((block) => block.type === 'planning');
4103
4298
 
4104
4299
  // Update history state with RAW content (actions applied at render time)
4105
4300
  setHistory((prev) => {
@@ -4121,6 +4316,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
4121
4316
  ...existingEntry,
4122
4317
  content: nextContent, // Store raw content without tool JSON or thinking tags
4123
4318
  callId: lastCallId || existingEntry.callId || '',
4319
+ artifactBlocks: mergedBlocks,
4320
+ planningBlocks,
4124
4321
  };
4125
4322
  // Keep ref in sync for callbacks (this doesn't trigger re-renders)
4126
4323
  latestHistoryRef.current = newHistory;
@@ -4346,106 +4543,113 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
4346
4543
  }
4347
4544
  }, [followOnPrompt, continueChat]);
4348
4545
 
4349
- // Monitor for errors from useLLM hook
4546
+ // Monitor for new errors from useLLM hook without replaying stale errors on new prompts.
4350
4547
  useEffect(() => {
4351
- if (llmError && llmError.trim()) {
4352
- // Skip if we've already processed this exact error
4353
- if (lastProcessedErrorRef.current === llmError) {
4354
- console.log('[AIChatPanel] Skipping duplicate error:', llmError);
4355
- return;
4356
- }
4357
-
4358
- console.log('[AIChatPanel] Error detected:', llmError);
4359
- lastProcessedErrorRef.current = llmError;
4360
-
4361
- // Parse error message to detect specific error types
4362
- const errorMessage = llmError;
4363
-
4364
- // Check if this is a user-initiated abort
4365
- const isAbortError = errorMessage.toLowerCase().includes('abort') ||
4366
- errorMessage.toLowerCase().includes('canceled') ||
4367
- errorMessage.toLowerCase().includes('cancelled');
4368
-
4369
- if (isAbortError) {
4370
- // User canceled the request - don't show error banner
4371
- console.log('[AIChatPanel] Request was aborted by user (useEffect)');
4372
- // Don't set error state - no red banner
4373
-
4374
- // Don't update history here - the error callback in send() already handled it
4375
- // with the correct promptKey. Updating here with lastKey can affect the wrong entry
4376
- // if the user has already submitted a new prompt.
4377
- }
4378
- // Detect 413 Content Too Large error
4379
- else if (errorMessage.includes('413') || errorMessage.toLowerCase().includes('content too large')) {
4380
- setError({
4381
- message: 'The context is too large to process. Please start a new conversation or reduce the amount of context.',
4382
- code: '413',
4548
+ const normalizedError = typeof llmError === 'string' ? llmError.trim() : '';
4549
+
4550
+ if (!normalizedError) {
4551
+ lastProcessedErrorRef.current = null;
4552
+ return;
4553
+ }
4554
+
4555
+ // Skip if we've already processed this exact error value from the hook.
4556
+ if (lastProcessedErrorRef.current === normalizedError) {
4557
+ console.log('[AIChatPanel] Skipping duplicate error:', normalizedError);
4558
+ return;
4559
+ }
4560
+
4561
+ console.log('[AIChatPanel] Error detected:', normalizedError);
4562
+ lastProcessedErrorRef.current = normalizedError;
4563
+
4564
+ const errorMessage = normalizedError;
4565
+ const currentLastKey = lastKeyRef.current;
4566
+ const currentLastCallId = lastCallIdRef.current;
4567
+
4568
+ // Check if this is a user-initiated abort
4569
+ const isAbortError =
4570
+ errorMessage.toLowerCase().includes('abort') ||
4571
+ errorMessage.toLowerCase().includes('canceled') ||
4572
+ errorMessage.toLowerCase().includes('cancelled');
4573
+
4574
+ if (isAbortError) {
4575
+ // User canceled the request - don't show error banner
4576
+ console.log('[AIChatPanel] Request was aborted by user (useEffect)');
4577
+ // Don't set error state - no red banner
4578
+
4579
+ // Don't update history here - the error callback in send() already handled it
4580
+ // with the correct promptKey. Updating here with lastKey can affect the wrong entry
4581
+ // if the user has already submitted a new prompt.
4582
+ }
4583
+ // Detect 413 Content Too Large error
4584
+ else if (errorMessage.includes('413') || errorMessage.toLowerCase().includes('content too large')) {
4585
+ setError({
4586
+ message: 'The context is too large to process. Please start a new conversation or reduce the amount of context.',
4587
+ code: '413',
4588
+ });
4589
+
4590
+ // Update history to show error
4591
+ if (currentLastKey) {
4592
+ setHistory((prev) => {
4593
+ const existingEntry = prev[currentLastKey] || { content: '', callId: '' };
4594
+ return {
4595
+ ...prev,
4596
+ [currentLastKey]: {
4597
+ ...existingEntry,
4598
+ content: `Error: ${errorMessage}`,
4599
+ callId: currentLastCallId || existingEntry.callId || '',
4600
+ },
4601
+ };
4383
4602
  });
4384
-
4385
- // Update history to show error
4386
- if (lastKey) {
4387
- setHistory((prev) => {
4388
- const existingEntry = prev[lastKey] || { content: '', callId: '' };
4389
- return {
4390
- ...prev,
4391
- [lastKey]: {
4392
- ...existingEntry,
4393
- content: `Error: ${errorMessage}`,
4394
- callId: lastCallId || existingEntry.callId || '',
4395
- },
4396
- };
4397
- });
4398
- }
4399
4603
  }
4400
- // Detect other network errors
4401
- else if (errorMessage.toLowerCase().includes('network error') || errorMessage.toLowerCase().includes('fetch')) {
4402
- setError({
4403
- message: 'Network error. Please check your connection and try again.',
4404
- code: 'NETWORK_ERROR',
4604
+ }
4605
+ // Detect other network errors
4606
+ else if (errorMessage.toLowerCase().includes('network error') || errorMessage.toLowerCase().includes('fetch')) {
4607
+ setError({
4608
+ message: 'Network error. Please check your connection and try again.',
4609
+ code: 'NETWORK_ERROR',
4610
+ });
4611
+
4612
+ // Update history to show error
4613
+ if (currentLastKey) {
4614
+ setHistory((prev) => {
4615
+ const existingEntry = prev[currentLastKey] || { content: '', callId: '' };
4616
+ return {
4617
+ ...prev,
4618
+ [currentLastKey]: {
4619
+ ...existingEntry,
4620
+ content: `Error: ${errorMessage}`,
4621
+ callId: currentLastCallId || existingEntry.callId || '',
4622
+ },
4623
+ };
4405
4624
  });
4406
-
4407
- // Update history to show error
4408
- if (lastKey) {
4409
- setHistory((prev) => {
4410
- const existingEntry = prev[lastKey] || { content: '', callId: '' };
4411
- return {
4412
- ...prev,
4413
- [lastKey]: {
4414
- ...existingEntry,
4415
- content: `Error: ${errorMessage}`,
4416
- callId: lastCallId || existingEntry.callId || '',
4417
- },
4418
- };
4419
- });
4420
- }
4421
4625
  }
4422
- // Generic error
4423
- else {
4424
- setError({
4425
- message: errorMessage,
4426
- code: 'UNKNOWN_ERROR',
4626
+ }
4627
+ // Generic error
4628
+ else {
4629
+ setError({
4630
+ message: errorMessage,
4631
+ code: 'UNKNOWN_ERROR',
4632
+ });
4633
+
4634
+ // Update history to show error
4635
+ if (currentLastKey) {
4636
+ setHistory((prev) => {
4637
+ const existingEntry = prev[currentLastKey] || { content: '', callId: '' };
4638
+ return {
4639
+ ...prev,
4640
+ [currentLastKey]: {
4641
+ ...existingEntry,
4642
+ content: `Error: ${errorMessage}`,
4643
+ callId: currentLastCallId || existingEntry.callId || '',
4644
+ },
4645
+ };
4427
4646
  });
4428
-
4429
- // Update history to show error
4430
- if (lastKey) {
4431
- setHistory((prev) => {
4432
- const existingEntry = prev[lastKey] || { content: '', callId: '' };
4433
- return {
4434
- ...prev,
4435
- [lastKey]: {
4436
- ...existingEntry,
4437
- content: `Error: ${errorMessage}`,
4438
- callId: lastCallId || existingEntry.callId || '',
4439
- },
4440
- };
4441
- });
4442
- }
4443
4647
  }
4444
-
4445
- // Reset loading state
4446
- setIsLoading(false);
4447
4648
  }
4448
- }, [llmError, lastKey, lastCallId]);
4649
+
4650
+ // Reset loading state
4651
+ setIsLoading(false);
4652
+ }, [llmError]);
4449
4653
 
4450
4654
  // Dynamic CSS Injection
4451
4655
  useEffect(() => {
@@ -4679,7 +4883,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
4679
4883
 
4680
4884
  const renderThinkingBlockCard = (
4681
4885
  entryKey: string,
4682
- block: { type: 'thinking' | 'reasoning' | 'searching'; content: string },
4886
+ block: { type: ArtifactBlockType; content: string },
4683
4887
  blockKey: string,
4684
4888
  renderKey: string,
4685
4889
  isStreaming: boolean,