@hef2024/llmasaservice-ui 0.25.0 → 0.25.2
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 +270 -55
- package/dist/index.mjs +270 -55
- package/package.json +1 -1
- package/src/AIAgentPanel.tsx +93 -7
- package/src/AIChatPanel.tsx +270 -55
package/src/AIChatPanel.tsx
CHANGED
|
@@ -2141,6 +2141,10 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2141
2141
|
const queuedToolRequestsRef = useRef<ToolRequestMatch[] | null>(null);
|
|
2142
2142
|
const suppressAbortHistoryUpdateRef = useRef<boolean>(false);
|
|
2143
2143
|
const toolReplaySummariesByKeyRef = useRef<Record<string, ToolReplaySummaryEntry[]>>({});
|
|
2144
|
+
const continuationDispatchPendingRef = useRef<boolean>(false);
|
|
2145
|
+
const successfulMutationSignaturesRef = useRef<Set<string>>(new Set());
|
|
2146
|
+
const queuedDrainTimerRef = useRef<number | null>(null);
|
|
2147
|
+
const queuedDrainScheduledRef = useRef<boolean>(false);
|
|
2144
2148
|
|
|
2145
2149
|
// Sync new entries from initialHistory into local history state
|
|
2146
2150
|
// This allows parent components to inject messages (e.g., page-based agent suggestions)
|
|
@@ -2798,6 +2802,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2798
2802
|
}, [customerEmailCaptureMode, emailInputSet]);
|
|
2799
2803
|
|
|
2800
2804
|
const pendingToolRequestsRef = useRef<ToolRequestMatch[]>(pendingToolRequests);
|
|
2805
|
+
const activeToolCallsRef = useRef(activeToolCalls);
|
|
2806
|
+
const turnLockRef = useRef<boolean>(false);
|
|
2801
2807
|
const streamIdleRef = useRef(idle);
|
|
2802
2808
|
streamIdleRef.current = idle;
|
|
2803
2809
|
|
|
@@ -2814,6 +2820,50 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2814
2820
|
pendingToolRequestsRef.current = pendingToolRequests;
|
|
2815
2821
|
}, [pendingToolRequests]);
|
|
2816
2822
|
|
|
2823
|
+
useEffect(() => {
|
|
2824
|
+
activeToolCallsRef.current = activeToolCalls;
|
|
2825
|
+
}, [activeToolCalls]);
|
|
2826
|
+
|
|
2827
|
+
const hasUnresolvedToolRequestsInLatestResponse = useCallback((): boolean => {
|
|
2828
|
+
const latestResponse = String(responseRef.current || '');
|
|
2829
|
+
if (!latestResponse.trim()) return false;
|
|
2830
|
+
const latestRequests = extractToolRequestMatchesFromText(latestResponse);
|
|
2831
|
+
if (latestRequests.length === 0) return false;
|
|
2832
|
+
|
|
2833
|
+
return latestRequests.some((request) => {
|
|
2834
|
+
const signature = getToolCallSignature(request.toolName, request.callId);
|
|
2835
|
+
if (!signature) return false;
|
|
2836
|
+
if (handledToolCallSignaturesRef.current.has(signature)) return false;
|
|
2837
|
+
if (inFlightToolCallSignaturesRef.current.has(signature)) return false;
|
|
2838
|
+
return true;
|
|
2839
|
+
});
|
|
2840
|
+
}, [getToolCallSignature]);
|
|
2841
|
+
|
|
2842
|
+
const hasInFlightTurnWork = useCallback((): boolean => {
|
|
2843
|
+
return (
|
|
2844
|
+
toolRequestProcessingRef.current ||
|
|
2845
|
+
continuationDispatchPendingRef.current ||
|
|
2846
|
+
(queuedToolRequestsRef.current || []).length > 0 ||
|
|
2847
|
+
(pendingToolRequestsRef.current || []).length > 0 ||
|
|
2848
|
+
(activeToolCallsRef.current || []).length > 0 ||
|
|
2849
|
+
!streamIdleRef.current ||
|
|
2850
|
+
hasUnresolvedToolRequestsInLatestResponse()
|
|
2851
|
+
);
|
|
2852
|
+
}, [hasUnresolvedToolRequestsInLatestResponse]);
|
|
2853
|
+
|
|
2854
|
+
const queuePromptForLater = useCallback((promptText: string) => {
|
|
2855
|
+
const normalizedPrompt = String(promptText || '').trim();
|
|
2856
|
+
if (!normalizedPrompt) return;
|
|
2857
|
+
const nextQueuedPrompts = [...queuedPromptsRef.current, normalizedPrompt];
|
|
2858
|
+
queuedPromptsRef.current = nextQueuedPrompts;
|
|
2859
|
+
setQueuedPrompts(nextQueuedPrompts);
|
|
2860
|
+
}, []);
|
|
2861
|
+
|
|
2862
|
+
const releaseTurnLockIfSettled = useCallback(() => {
|
|
2863
|
+
if (hasInFlightTurnWork()) return;
|
|
2864
|
+
turnLockRef.current = false;
|
|
2865
|
+
}, [hasInFlightTurnWork]);
|
|
2866
|
+
|
|
2817
2867
|
const processGivenToolRequests = useCallback(
|
|
2818
2868
|
async (requests: ToolRequestMatch[]) => {
|
|
2819
2869
|
const dedupeToolRequests = (input: ToolRequestMatch[]): ToolRequestMatch[] => {
|
|
@@ -2840,6 +2890,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2840
2890
|
return;
|
|
2841
2891
|
}
|
|
2842
2892
|
toolRequestProcessingRef.current = true;
|
|
2893
|
+
turnLockRef.current = true;
|
|
2843
2894
|
|
|
2844
2895
|
try {
|
|
2845
2896
|
let requestsToProcess = requests;
|
|
@@ -2931,6 +2982,61 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2931
2982
|
}
|
|
2932
2983
|
}
|
|
2933
2984
|
|
|
2985
|
+
const normalizeToolArgsForSignature = (value: unknown): unknown => {
|
|
2986
|
+
if (Array.isArray(value)) {
|
|
2987
|
+
return value.map((item) => normalizeToolArgsForSignature(item));
|
|
2988
|
+
}
|
|
2989
|
+
if (value && typeof value === 'object') {
|
|
2990
|
+
const normalizedObject: Record<string, unknown> = {};
|
|
2991
|
+
Object.keys(value as Record<string, unknown>)
|
|
2992
|
+
.sort()
|
|
2993
|
+
.forEach((key) => {
|
|
2994
|
+
normalizedObject[key] = normalizeToolArgsForSignature(
|
|
2995
|
+
(value as Record<string, unknown>)[key],
|
|
2996
|
+
);
|
|
2997
|
+
});
|
|
2998
|
+
return normalizedObject;
|
|
2999
|
+
}
|
|
3000
|
+
return value ?? null;
|
|
3001
|
+
};
|
|
3002
|
+
|
|
3003
|
+
const getSemanticMutationSignature = (
|
|
3004
|
+
toolName: string,
|
|
3005
|
+
args: Record<string, unknown>,
|
|
3006
|
+
): string => {
|
|
3007
|
+
const normalizedName = String(toolName || '').trim().toLowerCase();
|
|
3008
|
+
const normalizedArgs = normalizeToolArgsForSignature(args);
|
|
3009
|
+
return `${normalizedName}::${JSON.stringify(normalizedArgs)}`;
|
|
3010
|
+
};
|
|
3011
|
+
|
|
3012
|
+
const isLikelyMutatingCall = (
|
|
3013
|
+
toolName: string,
|
|
3014
|
+
args: Record<string, unknown>,
|
|
3015
|
+
): boolean => {
|
|
3016
|
+
const normalizedName = String(toolName || '').trim().toLowerCase();
|
|
3017
|
+
const action = String((args as Record<string, unknown>)?.action || '').trim().toLowerCase();
|
|
3018
|
+
const mutatingActions = new Set([
|
|
3019
|
+
'add',
|
|
3020
|
+
'create',
|
|
3021
|
+
'materialize_from_sourcing_result',
|
|
3022
|
+
'materialize',
|
|
3023
|
+
'update',
|
|
3024
|
+
'delete',
|
|
3025
|
+
'remove',
|
|
3026
|
+
'cancel',
|
|
3027
|
+
'complete',
|
|
3028
|
+
'move',
|
|
3029
|
+
]);
|
|
3030
|
+
|
|
3031
|
+
if (action && mutatingActions.has(action)) return true;
|
|
3032
|
+
return (
|
|
3033
|
+
normalizedName.includes('pipeline') ||
|
|
3034
|
+
normalizedName.includes('work_item') ||
|
|
3035
|
+
normalizedName.includes('candidate_pipeline') ||
|
|
3036
|
+
normalizedName.includes('cron')
|
|
3037
|
+
);
|
|
3038
|
+
};
|
|
3039
|
+
|
|
2934
3040
|
const parsedToolCalls = await Promise.all(
|
|
2935
3041
|
requestsToProcess.map(async (req, index) => {
|
|
2936
3042
|
let parsedToolCall: any = null;
|
|
@@ -2990,6 +3096,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2990
3096
|
|
|
2991
3097
|
const callSignature = getToolCallSignature(toolName, callId);
|
|
2992
3098
|
if (!callSignature) return null;
|
|
3099
|
+
const semanticMutationSignature = getSemanticMutationSignature(toolName, args);
|
|
3100
|
+
const likelyMutating = isLikelyMutatingCall(toolName, args);
|
|
2993
3101
|
|
|
2994
3102
|
return {
|
|
2995
3103
|
req,
|
|
@@ -2998,6 +3106,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2998
3106
|
args,
|
|
2999
3107
|
serviceTag,
|
|
3000
3108
|
callSignature,
|
|
3109
|
+
semanticMutationSignature,
|
|
3110
|
+
likelyMutating,
|
|
3001
3111
|
};
|
|
3002
3112
|
}),
|
|
3003
3113
|
);
|
|
@@ -3009,9 +3119,12 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3009
3119
|
args: Record<string, unknown>;
|
|
3010
3120
|
serviceTag: string;
|
|
3011
3121
|
callSignature: string;
|
|
3122
|
+
semanticMutationSignature: string;
|
|
3123
|
+
likelyMutating: boolean;
|
|
3012
3124
|
}>;
|
|
3013
3125
|
|
|
3014
3126
|
const seenCallSignatures = new Set<string>();
|
|
3127
|
+
const seenMutationSemanticsInBatch = new Set<string>();
|
|
3015
3128
|
const callsToRun: Array<{
|
|
3016
3129
|
req: ToolRequestMatch;
|
|
3017
3130
|
toolName: string;
|
|
@@ -3019,6 +3132,18 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3019
3132
|
args: Record<string, unknown>;
|
|
3020
3133
|
serviceTag: string;
|
|
3021
3134
|
callSignature: string;
|
|
3135
|
+
semanticMutationSignature: string;
|
|
3136
|
+
likelyMutating: boolean;
|
|
3137
|
+
}> = [];
|
|
3138
|
+
const suppressedMutationDuplicates: Array<{
|
|
3139
|
+
req: ToolRequestMatch;
|
|
3140
|
+
toolName: string;
|
|
3141
|
+
callId: string;
|
|
3142
|
+
args: Record<string, unknown>;
|
|
3143
|
+
serviceTag: string;
|
|
3144
|
+
callSignature: string;
|
|
3145
|
+
semanticMutationSignature: string;
|
|
3146
|
+
likelyMutating: boolean;
|
|
3022
3147
|
}> = [];
|
|
3023
3148
|
|
|
3024
3149
|
toolCallBatch.forEach((toolCall) => {
|
|
@@ -3027,11 +3152,25 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3027
3152
|
|
|
3028
3153
|
if (handledToolCallSignaturesRef.current.has(toolCall.callSignature)) return;
|
|
3029
3154
|
if (inFlightToolCallSignaturesRef.current.has(toolCall.callSignature)) return;
|
|
3155
|
+
if (toolCall.likelyMutating && seenMutationSemanticsInBatch.has(toolCall.semanticMutationSignature)) {
|
|
3156
|
+
suppressedMutationDuplicates.push(toolCall);
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
if (
|
|
3160
|
+
toolCall.likelyMutating &&
|
|
3161
|
+
successfulMutationSignaturesRef.current.has(toolCall.semanticMutationSignature)
|
|
3162
|
+
) {
|
|
3163
|
+
suppressedMutationDuplicates.push(toolCall);
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
if (toolCall.likelyMutating) {
|
|
3167
|
+
seenMutationSemanticsInBatch.add(toolCall.semanticMutationSignature);
|
|
3168
|
+
}
|
|
3030
3169
|
|
|
3031
3170
|
callsToRun.push(toolCall);
|
|
3032
3171
|
});
|
|
3033
3172
|
|
|
3034
|
-
if (callsToRun.length === 0) {
|
|
3173
|
+
if (callsToRun.length === 0 && suppressedMutationDuplicates.length === 0) {
|
|
3035
3174
|
setPendingToolRequests((prev) =>
|
|
3036
3175
|
prev.filter((request) => {
|
|
3037
3176
|
const signature = getToolCallSignature(request.toolName, request.callId);
|
|
@@ -3044,11 +3183,14 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3044
3183
|
return;
|
|
3045
3184
|
}
|
|
3046
3185
|
|
|
3047
|
-
const
|
|
3186
|
+
const handledBatchSignatures = new Set([
|
|
3187
|
+
...callsToRun.map((toolCall) => toolCall.callSignature),
|
|
3188
|
+
...suppressedMutationDuplicates.map((toolCall) => toolCall.callSignature),
|
|
3189
|
+
]);
|
|
3048
3190
|
setPendingToolRequests((prev) =>
|
|
3049
3191
|
prev.filter((request) => {
|
|
3050
3192
|
const signature = getToolCallSignature(request.toolName, request.callId);
|
|
3051
|
-
return !signature || !
|
|
3193
|
+
return !signature || !handledBatchSignatures.has(signature);
|
|
3052
3194
|
}),
|
|
3053
3195
|
);
|
|
3054
3196
|
callsToRun.forEach((toolCall) => {
|
|
@@ -3063,7 +3205,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3063
3205
|
})),
|
|
3064
3206
|
);
|
|
3065
3207
|
|
|
3066
|
-
const finalToolCalls = callsToRun.map((toolCall) => ({
|
|
3208
|
+
const finalToolCalls = [...callsToRun, ...suppressedMutationDuplicates].map((toolCall) => ({
|
|
3067
3209
|
id: toolCall.callId,
|
|
3068
3210
|
type: 'tool_use',
|
|
3069
3211
|
name: toolCall.toolName,
|
|
@@ -3233,12 +3375,32 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3233
3375
|
}),
|
|
3234
3376
|
);
|
|
3235
3377
|
|
|
3236
|
-
|
|
3378
|
+
const executedResponses = toolResponses.filter(Boolean) as any[];
|
|
3379
|
+
const suppressedResponses = suppressedMutationDuplicates.map((toolCall) => ({
|
|
3380
|
+
tool_call_id: toolCall.callId,
|
|
3381
|
+
tool_name: toolCall.toolName,
|
|
3382
|
+
result:
|
|
3383
|
+
'Duplicate mutating call suppressed: this exact action already succeeded earlier in the same turn.',
|
|
3384
|
+
isError: false,
|
|
3385
|
+
}));
|
|
3386
|
+
finalToolResponses = [...executedResponses, ...suppressedResponses];
|
|
3387
|
+
|
|
3388
|
+
callsToRun.forEach((toolCall) => {
|
|
3389
|
+
const matchedResponse = executedResponses.find(
|
|
3390
|
+
(response) => response?.tool_call_id === toolCall.callId,
|
|
3391
|
+
);
|
|
3392
|
+
if (!matchedResponse?.isError && toolCall.likelyMutating) {
|
|
3393
|
+
successfulMutationSignaturesRef.current.add(toolCall.semanticMutationSignature);
|
|
3394
|
+
}
|
|
3395
|
+
});
|
|
3237
3396
|
} finally {
|
|
3238
3397
|
callsToRun.forEach((toolCall) => {
|
|
3239
3398
|
inFlightToolCallSignaturesRef.current.delete(toolCall.callSignature);
|
|
3240
3399
|
handledToolCallSignaturesRef.current.add(toolCall.callSignature);
|
|
3241
3400
|
});
|
|
3401
|
+
suppressedMutationDuplicates.forEach((toolCall) => {
|
|
3402
|
+
handledToolCallSignaturesRef.current.add(toolCall.callSignature);
|
|
3403
|
+
});
|
|
3242
3404
|
}
|
|
3243
3405
|
|
|
3244
3406
|
// Keep the running state visible during execution; clear it only after completion.
|
|
@@ -3369,6 +3531,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3369
3531
|
if (!streamIdleRef.current) {
|
|
3370
3532
|
setActiveToolCalls([]);
|
|
3371
3533
|
setIsLoading(false);
|
|
3534
|
+
continuationDispatchPendingRef.current = false;
|
|
3372
3535
|
setError({
|
|
3373
3536
|
message: 'Timed out waiting for the previous stream to settle before tool continuation.',
|
|
3374
3537
|
code: 'TOOL_CONTINUATION_WAIT_TIMEOUT',
|
|
@@ -3388,6 +3551,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3388
3551
|
} else {
|
|
3389
3552
|
activeStreamAppendBaseRef.current = null;
|
|
3390
3553
|
}
|
|
3554
|
+
continuationDispatchPendingRef.current = true;
|
|
3391
3555
|
send(
|
|
3392
3556
|
'',
|
|
3393
3557
|
newMessages,
|
|
@@ -3405,12 +3569,14 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3405
3569
|
newController,
|
|
3406
3570
|
undefined,
|
|
3407
3571
|
(errorMsg: string) => {
|
|
3572
|
+
continuationDispatchPendingRef.current = false;
|
|
3408
3573
|
setActiveToolCalls([]);
|
|
3409
3574
|
setIsLoading(false);
|
|
3410
3575
|
setError({
|
|
3411
3576
|
message: errorMsg,
|
|
3412
3577
|
code: 'TOOL_ERROR',
|
|
3413
3578
|
});
|
|
3579
|
+
releaseTurnLockIfSettled();
|
|
3414
3580
|
},
|
|
3415
3581
|
);
|
|
3416
3582
|
} finally {
|
|
@@ -3421,6 +3587,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3421
3587
|
queueMicrotask(() => {
|
|
3422
3588
|
void processGivenToolRequests(queued);
|
|
3423
3589
|
});
|
|
3590
|
+
} else {
|
|
3591
|
+
releaseTurnLockIfSettled();
|
|
3424
3592
|
}
|
|
3425
3593
|
}
|
|
3426
3594
|
},
|
|
@@ -3440,6 +3608,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3440
3608
|
stop,
|
|
3441
3609
|
lastController,
|
|
3442
3610
|
waitForStreamIdle,
|
|
3611
|
+
releaseTurnLockIfSettled,
|
|
3443
3612
|
],
|
|
3444
3613
|
);
|
|
3445
3614
|
|
|
@@ -3469,6 +3638,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3469
3638
|
|
|
3470
3639
|
useEffect(() => {
|
|
3471
3640
|
if (pendingToolRequests.length === 0) return;
|
|
3641
|
+
if (!idle) return;
|
|
3642
|
+
if (continuationDispatchPendingRef.current) return;
|
|
3472
3643
|
|
|
3473
3644
|
const configuredAutoApproveTools = Array.isArray(autoApproveTools)
|
|
3474
3645
|
? new Set(
|
|
@@ -3503,6 +3674,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3503
3674
|
}
|
|
3504
3675
|
}, [
|
|
3505
3676
|
autoApproveTools,
|
|
3677
|
+
idle,
|
|
3506
3678
|
pendingToolRequests,
|
|
3507
3679
|
sessionApprovedTools,
|
|
3508
3680
|
alwaysApprovedTools,
|
|
@@ -3955,11 +4127,24 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3955
4127
|
// Continue chat (send message) - matches ChatPanel behavior exactly
|
|
3956
4128
|
// promptText is now required - comes from the isolated ChatInput component
|
|
3957
4129
|
const continueChat = useCallback((promptText: string) => {
|
|
4130
|
+
const promptToSend = String(promptText || '').trim();
|
|
4131
|
+
if (!promptToSend) return;
|
|
4132
|
+
|
|
4133
|
+
// Hard serialization guard: never start a second send while the current turn
|
|
4134
|
+
// is still settling (including tool/continuation phases).
|
|
4135
|
+
if (turnLockRef.current || hasInFlightTurnWork()) {
|
|
4136
|
+
queuePromptForLater(promptToSend);
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
turnLockRef.current = true;
|
|
4140
|
+
|
|
3958
4141
|
handledToolCallSignaturesRef.current = new Set();
|
|
3959
4142
|
inFlightToolCallSignaturesRef.current = new Set();
|
|
3960
4143
|
toolContinuationCountRef.current = 0;
|
|
4144
|
+
successfulMutationSignaturesRef.current = new Set();
|
|
3961
4145
|
activeStreamAppendBaseRef.current = null;
|
|
3962
4146
|
toolReplaySummariesByKeyRef.current = {};
|
|
4147
|
+
continuationDispatchPendingRef.current = false;
|
|
3963
4148
|
setPendingToolRequests([]);
|
|
3964
4149
|
setActiveToolCalls([]);
|
|
3965
4150
|
|
|
@@ -3993,33 +4178,16 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3993
4178
|
// when the history update effect runs
|
|
3994
4179
|
setResponse('');
|
|
3995
4180
|
|
|
3996
|
-
// Handle stop if not idle (matches ChatPanel)
|
|
3997
|
-
if (!idle) {
|
|
3998
|
-
stop(lastController);
|
|
3999
|
-
setHistory((prevHistory) => ({
|
|
4000
|
-
...prevHistory,
|
|
4001
|
-
[lastKey ?? '']: {
|
|
4002
|
-
content: processThinkingTags(response).cleanedText + '\n\n(response cancelled)',
|
|
4003
|
-
callId: lastCallId || '',
|
|
4004
|
-
},
|
|
4005
|
-
}));
|
|
4006
|
-
return;
|
|
4007
|
-
}
|
|
4008
|
-
|
|
4009
4181
|
if (clearFollowOnQuestionsNextPrompt) {
|
|
4010
4182
|
setFollowOnQuestionsState([]);
|
|
4011
4183
|
}
|
|
4012
|
-
|
|
4013
|
-
const promptToSend = promptText;
|
|
4014
|
-
|
|
4015
|
-
if (!promptToSend || !promptToSend.trim()) return;
|
|
4016
|
-
|
|
4184
|
+
|
|
4017
4185
|
setIsLoading(true);
|
|
4018
4186
|
|
|
4019
4187
|
// === OPTIMISTIC UPDATE: Show prompt immediately in UI ===
|
|
4020
4188
|
// Generate unique key using ISO timestamp prefix + prompt
|
|
4021
4189
|
const timestamp = new Date().toISOString();
|
|
4022
|
-
const promptKey = `${timestamp}:${promptToSend
|
|
4190
|
+
const promptKey = `${timestamp}:${promptToSend}`;
|
|
4023
4191
|
|
|
4024
4192
|
// Add prompt to history IMMEDIATELY - this makes it appear in the UI right away
|
|
4025
4193
|
setHistory((prevHistory) => ({
|
|
@@ -4029,7 +4197,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4029
4197
|
toolReplaySummariesByKeyRef.current[promptKey] = [];
|
|
4030
4198
|
|
|
4031
4199
|
// Store the key for later use
|
|
4032
|
-
setLastPrompt(promptToSend
|
|
4200
|
+
setLastPrompt(promptToSend);
|
|
4033
4201
|
setLastKey(promptKey);
|
|
4034
4202
|
|
|
4035
4203
|
// Scroll to bottom immediately to show the new prompt
|
|
@@ -4060,7 +4228,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4060
4228
|
|
|
4061
4229
|
// Build the full prompt - only apply template for first message (matches ChatPanel)
|
|
4062
4230
|
// Check if this is the first message by seeing if messagesAndHistory is empty
|
|
4063
|
-
let fullPromptToSend = promptToSend
|
|
4231
|
+
let fullPromptToSend = promptToSend;
|
|
4064
4232
|
if (messagesAndHistory.length === 0 && promptTemplate) {
|
|
4065
4233
|
fullPromptToSend = promptTemplate.replace('{{prompt}}', fullPromptToSend);
|
|
4066
4234
|
}
|
|
@@ -4071,7 +4239,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4071
4239
|
if (onBeforeSend) {
|
|
4072
4240
|
void Promise.resolve(
|
|
4073
4241
|
onBeforeSend({
|
|
4074
|
-
prompt: promptToSend
|
|
4242
|
+
prompt: promptToSend,
|
|
4075
4243
|
conversationId: convId || null,
|
|
4076
4244
|
agentId: agent,
|
|
4077
4245
|
service,
|
|
@@ -4110,6 +4278,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4110
4278
|
if (isAbortError) {
|
|
4111
4279
|
if (suppressAbortHistoryUpdateRef.current) {
|
|
4112
4280
|
setIsLoading(false);
|
|
4281
|
+
releaseTurnLockIfSettled();
|
|
4113
4282
|
return;
|
|
4114
4283
|
}
|
|
4115
4284
|
// User canceled the request - don't show error banner
|
|
@@ -4200,6 +4369,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4200
4369
|
|
|
4201
4370
|
// Reset loading state
|
|
4202
4371
|
setIsLoading(false);
|
|
4372
|
+
releaseTurnLockIfSettled();
|
|
4203
4373
|
}
|
|
4204
4374
|
);
|
|
4205
4375
|
|
|
@@ -4213,15 +4383,16 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4213
4383
|
onConversationCreated(convId);
|
|
4214
4384
|
}, 100);
|
|
4215
4385
|
}
|
|
4386
|
+
}).catch((error) => {
|
|
4387
|
+
console.error('[AIChatPanel] Failed to send prompt:', error);
|
|
4388
|
+
setError({
|
|
4389
|
+
message: error instanceof Error ? error.message : 'Failed to send prompt',
|
|
4390
|
+
code: 'UNKNOWN_ERROR',
|
|
4391
|
+
});
|
|
4392
|
+
setIsLoading(false);
|
|
4393
|
+
releaseTurnLockIfSettled();
|
|
4216
4394
|
});
|
|
4217
4395
|
}, [
|
|
4218
|
-
idle,
|
|
4219
|
-
stop,
|
|
4220
|
-
lastController,
|
|
4221
|
-
lastKey,
|
|
4222
|
-
response,
|
|
4223
|
-
lastCallId,
|
|
4224
|
-
processThinkingTags,
|
|
4225
4396
|
clearFollowOnQuestionsNextPrompt,
|
|
4226
4397
|
promptTemplate,
|
|
4227
4398
|
send,
|
|
@@ -4235,18 +4406,17 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4235
4406
|
scrollToBottom,
|
|
4236
4407
|
onConversationCreated,
|
|
4237
4408
|
onBeforeSend,
|
|
4409
|
+
hasInFlightTurnWork,
|
|
4410
|
+
queuePromptForLater,
|
|
4411
|
+
releaseTurnLockIfSettled,
|
|
4238
4412
|
getThinkingBlockCollapseKey,
|
|
4239
4413
|
getThinkingBlockRenderKey,
|
|
4240
4414
|
setResponse,
|
|
4241
4415
|
]);
|
|
4242
4416
|
|
|
4243
4417
|
const handleQueuePrompt = useCallback((promptText: string) => {
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
const nextQueuedPrompts = [...queuedPromptsRef.current, normalizedPrompt];
|
|
4247
|
-
queuedPromptsRef.current = nextQueuedPrompts;
|
|
4248
|
-
setQueuedPrompts(nextQueuedPrompts);
|
|
4249
|
-
}, []);
|
|
4418
|
+
queuePromptForLater(promptText);
|
|
4419
|
+
}, [queuePromptForLater]);
|
|
4250
4420
|
|
|
4251
4421
|
const handleClearQueuedPrompt = useCallback((index: number) => {
|
|
4252
4422
|
const nextQueuedPrompts = queuedPromptsRef.current.filter((_, queueIndex) => queueIndex !== index);
|
|
@@ -4264,20 +4434,6 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4264
4434
|
stop(lastController);
|
|
4265
4435
|
}, [stop, lastController]);
|
|
4266
4436
|
|
|
4267
|
-
useEffect(() => {
|
|
4268
|
-
const nextQueuedPrompt = String(queuedPromptsRef.current[0] || '').trim();
|
|
4269
|
-
if (!nextQueuedPrompt) return;
|
|
4270
|
-
if (!idle || isLoading) return;
|
|
4271
|
-
if (pendingToolRequests.length > 0 || activeToolCalls.length > 0) return;
|
|
4272
|
-
if (toolRequestProcessingRef.current) return;
|
|
4273
|
-
if ((queuedToolRequestsRef.current || []).length > 0) return;
|
|
4274
|
-
|
|
4275
|
-
const remainingQueuedPrompts = queuedPromptsRef.current.slice(1);
|
|
4276
|
-
queuedPromptsRef.current = remainingQueuedPrompts;
|
|
4277
|
-
setQueuedPrompts(remainingQueuedPrompts);
|
|
4278
|
-
continueChat(nextQueuedPrompt);
|
|
4279
|
-
}, [activeToolCalls.length, continueChat, idle, isLoading, pendingToolRequests.length, queuedPrompts.length]);
|
|
4280
|
-
|
|
4281
4437
|
// Reset conversation
|
|
4282
4438
|
const handleNewConversation = useCallback(() => {
|
|
4283
4439
|
if (!newConversationConfirm) {
|
|
@@ -4323,6 +4479,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4323
4479
|
setUserHasScrolled(false);
|
|
4324
4480
|
setError(null); // Clear any errors
|
|
4325
4481
|
setActiveToolCalls([]);
|
|
4482
|
+
turnLockRef.current = false;
|
|
4326
4483
|
|
|
4327
4484
|
setTimeout(() => {
|
|
4328
4485
|
setJustReset(false);
|
|
@@ -4354,6 +4511,9 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4354
4511
|
return true;
|
|
4355
4512
|
});
|
|
4356
4513
|
|
|
4514
|
+
// Keep ref synchronized immediately so completion/drain guards evaluate
|
|
4515
|
+
// against the same response pass, not the previous render.
|
|
4516
|
+
pendingToolRequestsRef.current = unseenToolRequests;
|
|
4357
4517
|
setPendingToolRequests((prev) => {
|
|
4358
4518
|
if (areToolRequestListsEqual(prev, unseenToolRequests)) {
|
|
4359
4519
|
return prev;
|
|
@@ -4483,11 +4643,14 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4483
4643
|
responseCompleteCallbackRef.current(currentLastCallId, currentLastPrompt || '', entry.content);
|
|
4484
4644
|
}
|
|
4485
4645
|
}
|
|
4646
|
+
|
|
4647
|
+
releaseTurnLockIfSettled();
|
|
4486
4648
|
}
|
|
4487
4649
|
|
|
4488
4650
|
// Reset notification flag when starting a new stream
|
|
4489
4651
|
if (!isNowIdle && hasNotifiedCompletionRef.current) {
|
|
4490
4652
|
hasNotifiedCompletionRef.current = false;
|
|
4653
|
+
continuationDispatchPendingRef.current = false;
|
|
4491
4654
|
// Reset response length tracking for new stream
|
|
4492
4655
|
prevResponseLengthRef.current = 0;
|
|
4493
4656
|
const currentLastKey = lastKeyRef.current;
|
|
@@ -4504,7 +4667,59 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4504
4667
|
// Keep thinking UI state across follow-on streams triggered by tool calls.
|
|
4505
4668
|
// New user prompts already reset this state explicitly before send().
|
|
4506
4669
|
}
|
|
4507
|
-
}, [idle]); // ONLY depends on idle
|
|
4670
|
+
}, [idle, releaseTurnLockIfSettled]); // ONLY depends on idle transition + lock-release guard
|
|
4671
|
+
|
|
4672
|
+
// Reconcile lock state on idle renders where tool markers/cards are cleared without
|
|
4673
|
+
// another idle transition (e.g., follow-up response replaces a prior tool-only payload).
|
|
4674
|
+
useEffect(() => {
|
|
4675
|
+
if (!idle) return;
|
|
4676
|
+
releaseTurnLockIfSettled();
|
|
4677
|
+
}, [idle, response, pendingToolRequests.length, activeToolCalls.length, releaseTurnLockIfSettled]);
|
|
4678
|
+
|
|
4679
|
+
// Drain queued prompts only after response history has been reconciled.
|
|
4680
|
+
// This effect intentionally sits after the response-processing/completion effects above
|
|
4681
|
+
// so queued sends inherit the latest finalized assistant turn context.
|
|
4682
|
+
useEffect(() => {
|
|
4683
|
+
const nextQueuedPrompt = String(queuedPromptsRef.current[0] || '').trim();
|
|
4684
|
+
if (!nextQueuedPrompt) return;
|
|
4685
|
+
if (!idle || isLoading) return;
|
|
4686
|
+
if (pendingToolRequests.length > 0 || activeToolCalls.length > 0) return;
|
|
4687
|
+
if (turnLockRef.current || hasInFlightTurnWork()) return;
|
|
4688
|
+
if (queuedDrainScheduledRef.current) return;
|
|
4689
|
+
|
|
4690
|
+
queuedDrainScheduledRef.current = true;
|
|
4691
|
+
queuedDrainTimerRef.current = window.setTimeout(() => {
|
|
4692
|
+
queuedDrainScheduledRef.current = false;
|
|
4693
|
+
queuedDrainTimerRef.current = null;
|
|
4694
|
+
|
|
4695
|
+
const queuedPrompt = String(queuedPromptsRef.current[0] || '').trim();
|
|
4696
|
+
if (!queuedPrompt) return;
|
|
4697
|
+
if (turnLockRef.current || hasInFlightTurnWork()) return;
|
|
4698
|
+
|
|
4699
|
+
const remainingQueuedPrompts = queuedPromptsRef.current.slice(1);
|
|
4700
|
+
queuedPromptsRef.current = remainingQueuedPrompts;
|
|
4701
|
+
setQueuedPrompts(remainingQueuedPrompts);
|
|
4702
|
+
continueChat(queuedPrompt);
|
|
4703
|
+
}, 0);
|
|
4704
|
+
}, [
|
|
4705
|
+
continueChat,
|
|
4706
|
+
hasInFlightTurnWork,
|
|
4707
|
+
idle,
|
|
4708
|
+
isLoading,
|
|
4709
|
+
pendingToolRequests.length,
|
|
4710
|
+
activeToolCalls.length,
|
|
4711
|
+
queuedPrompts.length,
|
|
4712
|
+
]);
|
|
4713
|
+
|
|
4714
|
+
useEffect(() => {
|
|
4715
|
+
return () => {
|
|
4716
|
+
if (queuedDrainTimerRef.current !== null) {
|
|
4717
|
+
window.clearTimeout(queuedDrainTimerRef.current);
|
|
4718
|
+
queuedDrainTimerRef.current = null;
|
|
4719
|
+
}
|
|
4720
|
+
queuedDrainScheduledRef.current = false;
|
|
4721
|
+
};
|
|
4722
|
+
}, []);
|
|
4508
4723
|
|
|
4509
4724
|
// Keep refs in sync
|
|
4510
4725
|
useEffect(() => {
|