@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.4.1 → 0.4.3

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.
@@ -5,6 +5,8 @@ import * as React from 'react';
5
5
  import { flushSync } from 'react-dom';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { createNxtlinqApi, setApiHosts, synthesizeSpeechToBuffer, streamSpeechToAudioContext, useLocalStorage, useSessionStorage, useSpeechToTextFromMic, useVoiceMode, metakeepClient, getEthers, sleep, walletTextUtils, } from '@bytexbyte/nxtlinq-ai-agent-web-development';
8
+ import { authorizeTextFrontendTool, hasRecordedWalletVerification, validateWalletSession, } from '@bytexbyte/nxtlinq-ai-agent-core-development';
9
+ import { PendingTextToolRetryController } from '../pendingTextToolRetry';
8
10
  const MIC_ENABLED_SESSION_KEY = 'chatbot-mic-enabled';
9
11
  const ChatBotContext = React.createContext(undefined);
10
12
  export const useChatBot = () => {
@@ -143,6 +145,17 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
143
145
  const permissionsRef = React.useRef(permissions);
144
146
  const nxtlinqAITServiceAccessTokenRef = React.useRef(nxtlinqAITServiceAccessToken);
145
147
  const signerRef = React.useRef(signer);
148
+ const pendingTextToolRetryRef = React.useRef(new PendingTextToolRetryController());
149
+ const sendMessageRef = React.useRef(async () => { });
150
+ const retryPendingTextTool = React.useCallback(() => {
151
+ const controller = pendingTextToolRetryRef.current;
152
+ return controller.retry(async (request) => {
153
+ // This attempt consumes the old request. A still-blocked preflight will
154
+ // record it again with the next required recovery step.
155
+ controller.clear();
156
+ await sendMessageRef.current(request.content, 1, request.isPresetMessage, request.attachments);
157
+ });
158
+ }, []);
146
159
  // Helper function to combine customUsername with Adilas customUserInfo
147
160
  const getFinalCustomUsername = React.useCallback((username) => {
148
161
  if (!username)
@@ -859,7 +872,9 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
859
872
  };
860
873
  // Refresh AIT
861
874
  const refreshAIT = async (forceUpdatePermissions = false) => {
862
- if (!hitAddress) {
875
+ const currentHitAddress = hitAddressRef.current || hitAddress;
876
+ const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
877
+ if (!currentHitAddress) {
863
878
  setAit(null);
864
879
  setPermissions([]);
865
880
  setWalletInfo(null);
@@ -868,14 +883,18 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
868
883
  }
869
884
  setIsAITLoading(true);
870
885
  try {
886
+ let walletAllowsAITLookup = !requireWalletIDVVerification;
871
887
  // Get wallet info first - always try to get wallet info if we have a token
872
- if (nxtlinqAITServiceAccessToken) {
888
+ if (currentToken) {
873
889
  try {
874
- const walletResponse = await nxtlinqApi.wallet.getWallet({ address: hitAddress }, nxtlinqAITServiceAccessToken);
890
+ const walletResponse = await nxtlinqApi.wallet.getWallet({ address: currentHitAddress }, currentToken);
875
891
  if (!('error' in walletResponse)) {
876
892
  setWalletInfo(walletResponse);
893
+ walletAllowsAITLookup = !requireWalletIDVVerification
894
+ || hasRecordedWalletVerification(walletResponse);
877
895
  }
878
896
  else {
897
+ setWalletInfo(null);
879
898
  // Check if the error is due to invalid/expired token
880
899
  if (walletResponse.error.includes('Invalid or expired token')) {
881
900
  console.log('Token appears to be invalid during wallet info fetch, clearing it');
@@ -884,16 +903,24 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
884
903
  }
885
904
  }
886
905
  catch (error) {
906
+ setWalletInfo(null);
887
907
  console.error('Failed to fetch wallet info:', error);
888
908
  }
889
909
  }
910
+ // AIT existence is not identity verification. In strict mode, stop before
911
+ // the legacy AIT lookup can synthesize a custom wallet record.
912
+ if (currentToken && !walletAllowsAITLookup) {
913
+ setAit(null);
914
+ setPermissions([]);
915
+ return;
916
+ }
890
917
  // Only try to fetch AIT if we have a token
891
- if (nxtlinqAITServiceAccessToken) {
918
+ if (currentToken) {
892
919
  const response = await nxtlinqApi.ait.getAITByServiceIdAndController({
893
920
  serviceId,
894
- controller: hitAddress,
921
+ controller: currentHitAddress,
895
922
  customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
896
- }, nxtlinqAITServiceAccessToken);
923
+ }, currentToken);
897
924
  if ('error' in response) {
898
925
  console.error('Failed to fetch AIT:', response.error);
899
926
  setAit(null);
@@ -927,17 +954,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
927
954
  return false;
928
955
  if (!nxtlinqAITServiceAccessToken)
929
956
  return true;
930
- try {
931
- const payload = JSON.parse(atob(nxtlinqAITServiceAccessToken.split('.')[1]));
932
- const address = payload.address;
933
- if (address !== hitAddress)
934
- return true;
935
- return false;
936
- }
937
- catch (error) {
938
- console.error('Error parsing token:', error);
939
- return true;
940
- }
957
+ return !validateWalletSession(nxtlinqAITServiceAccessToken, hitAddress).valid;
941
958
  }, [hitAddress, nxtlinqAITServiceAccessToken]);
942
959
  // Connect wallet
943
960
  const connectWallet = React.useCallback(async (autoShowSignInMessage = true) => {
@@ -1067,14 +1084,19 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1067
1084
  if (autoShowSuccessMessage) {
1068
1085
  showError(walletTextUtils.getWalletText('Please connect your wallet first.', serviceId));
1069
1086
  }
1070
- return;
1087
+ return false;
1071
1088
  }
1072
1089
  if (!signerToUse) {
1073
1090
  console.log('No signer available, returning early');
1074
1091
  if (autoShowSuccessMessage) {
1075
1092
  showError(walletTextUtils.getWalletText('Please connect your wallet first.', serviceId));
1076
1093
  }
1077
- return;
1094
+ return false;
1095
+ }
1096
+ const currentToken = nxtlinqAITServiceAccessTokenRef.current;
1097
+ if (currentToken && validateWalletSession(currentToken, addressToUse).valid) {
1098
+ await refreshAIT();
1099
+ return true;
1078
1100
  }
1079
1101
  try {
1080
1102
  const nonceResponse = await nxtlinqApi.auth.getNonce({ address: addressToUse });
@@ -1082,7 +1104,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1082
1104
  if (autoShowSuccessMessage) {
1083
1105
  showError(normalizeErrorForUser(nonceResponse.error));
1084
1106
  }
1085
- return;
1107
+ return false;
1086
1108
  }
1087
1109
  const payload = {
1088
1110
  address: addressToUse,
@@ -1099,357 +1121,91 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1099
1121
  if (autoShowSuccessMessage) {
1100
1122
  showError(normalizeErrorForUser(response.error));
1101
1123
  }
1102
- return;
1124
+ return false;
1103
1125
  }
1104
1126
  const { accessToken } = response;
1105
1127
  setNxtlinqAITServiceAccessToken(accessToken);
1128
+ nxtlinqAITServiceAccessTokenRef.current = accessToken;
1106
1129
  // Wait for state to update before refreshing AIT
1107
1130
  await new Promise(resolve => setTimeout(resolve, 100));
1108
1131
  await refreshAIT();
1109
1132
  if (autoShowSuccessMessage) {
1110
1133
  showSuccess(walletTextUtils.getWalletText('Successfully signed in with your HIT wallet. You can now use the AI agent.', serviceId));
1111
1134
  }
1135
+ return true;
1112
1136
  }
1113
1137
  catch (error) {
1114
1138
  console.error('Failed to sign in:', error);
1115
1139
  if (autoShowSuccessMessage) {
1116
1140
  showError('Failed to sign in. Please try again.');
1117
1141
  }
1142
+ return false;
1118
1143
  }
1119
1144
  };
1120
1145
  // Check permissions
1121
- const hasPermission = async (requiredPermission, autoRetry = true, onAutoConnect, onAutoSignIn) => {
1146
+ const hasPermission = async (requiredPermission) => {
1122
1147
  // Use refs to get latest state values
1123
1148
  const currentHitAddress = hitAddressRef.current;
1124
- const currentAit = aitRef.current;
1125
- const currentPermissions = permissionsRef.current;
1126
1149
  const currentToken = nxtlinqAITServiceAccessTokenRef.current;
1127
- if (!currentHitAddress) {
1128
- if (autoRetry) {
1129
- setIsLoading(false); // Stop thinking before showing message
1130
- setMessages(prev => [...prev, {
1131
- id: Date.now().toString(),
1132
- content: walletTextUtils.getWalletText('Please connect your HIT wallet to continue.', serviceId),
1133
- role: 'assistant',
1134
- timestamp: new Date().toISOString(),
1135
- button: 'connectWallet'
1136
- }]);
1137
- try {
1138
- setIsAutoConnecting(true); // Mark as auto-connecting
1139
- await connectWallet(false); // Don't show sign-in message yet
1140
- setIsAutoConnecting(false); // Clear auto-connecting state
1141
- // Show brief success message for auto-connect
1142
- showSuccess(walletTextUtils.getWalletText('Auto wallet connection successful', serviceId));
1143
- onAutoConnect?.(); // Call callback if provided
1144
- await new Promise(resolve => setTimeout(resolve, 2000));
1145
- // After auto connect, if not signed in, then auto sign-in
1146
- const tokenAfterConnect = nxtlinqAITServiceAccessTokenRef.current;
1147
- if (!tokenAfterConnect) {
1148
- setIsAutoConnecting(true);
1149
- await signInWallet(false);
1150
- onAutoSignIn?.(); // Call callback if provided
1151
- setIsAutoConnecting(false);
1152
- showSuccess(walletTextUtils.getWalletText('Auto sign-in successful after wallet connect', serviceId));
1153
- await refreshAIT();
1154
- // Wait for AIT to be fully loaded with polling
1155
- let attempts = 0;
1156
- const maxAttempts = 5;
1157
- while (!aitRef.current && attempts < maxAttempts) {
1158
- await new Promise(resolve => setTimeout(resolve, 2000));
1159
- attempts++;
1160
- }
1161
- }
1162
- // If connection (and sign-in if needed) successful, continue with permission check
1163
- const result = await hasPermission(requiredPermission, false);
1164
- return result;
1165
- }
1166
- catch (error) {
1167
- console.error('Failed to auto-connect wallet:', error);
1168
- setIsAutoConnecting(false); // Clear auto-connecting state on error
1169
- return false;
1170
- }
1171
- }
1172
- // If autoRetry is false, don't show message again, just return false
1173
- return false;
1174
- }
1175
- if (!currentToken) {
1176
- if (autoRetry) {
1177
- setIsLoading(false); // Stop thinking before showing message
1178
- setMessages(prev => [...prev, {
1179
- id: Date.now().toString(),
1180
- content: walletTextUtils.getWalletText('Please sign in with your HIT wallet to continue.', serviceId),
1181
- role: 'assistant',
1182
- timestamp: new Date().toISOString(),
1183
- button: 'signIn'
1184
- }]);
1185
- try {
1186
- setIsAutoConnecting(true); // Mark as auto-signing
1187
- await signInWallet(false); // Don't show success message yet
1188
- onAutoSignIn?.(); // Call callback if provided
1189
- setIsAutoConnecting(false); // Clear auto-signing state
1190
- // Show brief success message for auto-sign-in
1191
- showSuccess('Auto sign-in successful');
1192
- // Ensure AIT is refreshed after sign-in
1193
- await refreshAIT();
1194
- // Wait for AIT to be fully loaded with polling
1195
- let attempts = 0;
1196
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
1197
- while (!aitRef.current && attempts < maxAttempts) {
1198
- await new Promise(resolve => setTimeout(resolve, 2000));
1199
- attempts++;
1200
- }
1201
- // Only continue if AIT is actually loaded
1202
- if (aitRef.current) {
1203
- // Wait a bit more to ensure permissions are also loaded
1204
- await new Promise(resolve => setTimeout(resolve, 2000));
1205
- // If sign-in successful, continue with permission check
1206
- const result = await hasPermission(requiredPermission, false);
1207
- return result;
1208
- }
1209
- else {
1210
- return false;
1211
- }
1212
- }
1213
- catch (error) {
1214
- console.error('Failed to auto-sign-in wallet:', error);
1215
- setIsAutoConnecting(false); // Clear auto-signing state on error
1216
- return false;
1217
- }
1218
- }
1219
- // If autoRetry is false, don't show message again, just return false
1220
- return false;
1221
- }
1222
- try {
1223
- const payload = JSON.parse(atob(currentToken.split('.')[1]));
1224
- const address = payload.address;
1225
- if (address !== currentHitAddress) {
1226
- setNxtlinqAITServiceAccessToken('');
1227
- if (autoRetry) {
1228
- setIsLoading(false); // Stop thinking before showing message
1229
- setMessages(prev => [...prev, {
1230
- id: Date.now().toString(),
1231
- content: walletTextUtils.getWalletText('Wallet address mismatch. Please sign in with the correct wallet.', serviceId),
1232
- role: 'assistant',
1233
- timestamp: new Date().toISOString(),
1234
- button: 'signIn'
1235
- }]);
1236
- try {
1237
- setIsAutoConnecting(true); // Mark as auto-signing
1238
- await signInWallet(false); // Don't show success message yet
1239
- onAutoSignIn?.(); // Call callback if provided
1240
- setIsAutoConnecting(false); // Clear auto-signing state
1241
- // Show brief success message for auto-sign-in after address mismatch
1242
- showSuccess('Auto sign-in successful after address mismatch');
1243
- // Ensure AIT is refreshed after sign-in
1244
- await refreshAIT();
1245
- // Wait for AIT to be fully loaded with polling
1246
- let attempts = 0;
1247
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
1248
- while (!aitRef.current && attempts < maxAttempts) {
1249
- await new Promise(resolve => setTimeout(resolve, 2000));
1250
- attempts++;
1251
- }
1252
- // Only continue if AIT is actually loaded
1253
- if (aitRef.current) {
1254
- // Wait a bit more to ensure permissions are also loaded
1255
- await new Promise(resolve => setTimeout(resolve, 2000));
1256
- // If sign-in successful, continue with permission check
1257
- const result = await hasPermission(requiredPermission, false);
1258
- return result;
1259
- }
1260
- else {
1261
- return false;
1262
- }
1263
- }
1264
- catch (error) {
1265
- console.error('Failed to auto-sign-in after address mismatch:', error);
1266
- setIsAutoConnecting(false); // Clear auto-signing state on error
1267
- return false;
1268
- }
1269
- }
1270
- // If autoRetry is false, don't show message again, just return false
1271
- return false;
1272
- }
1273
- }
1274
- catch (error) {
1275
- console.error('Error parsing token:', error);
1276
- setNxtlinqAITServiceAccessToken('');
1277
- if (autoRetry) {
1278
- setIsLoading(false); // Stop thinking before showing message
1279
- setMessages(prev => [...prev, {
1280
- id: Date.now().toString(),
1281
- content: walletTextUtils.getWalletText('Invalid wallet session. Please sign in again.', serviceId),
1282
- role: 'assistant',
1283
- timestamp: new Date().toISOString(),
1284
- button: 'signIn'
1285
- }]);
1286
- try {
1287
- setIsAutoConnecting(true); // Mark as auto-signing
1288
- await signInWallet(false); // Don't show success message yet
1289
- onAutoSignIn?.(); // Call callback if provided
1290
- setIsAutoConnecting(false); // Clear auto-signing state
1291
- // Show brief success message for auto-sign-in after token parse error
1292
- showSuccess('Auto sign-in successful after token error');
1293
- // Ensure AIT is refreshed after sign-in
1294
- await refreshAIT();
1295
- // Wait for AIT to be fully loaded with polling
1296
- let attempts = 0;
1297
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
1298
- while (!aitRef.current && attempts < maxAttempts) {
1299
- await new Promise(resolve => setTimeout(resolve, 2000));
1300
- attempts++;
1301
- }
1302
- // Only continue if AIT is actually loaded
1303
- if (aitRef.current) {
1304
- // Wait a bit more to ensure permissions are also loaded
1305
- await new Promise(resolve => setTimeout(resolve, 2000));
1306
- // If sign-in successful, continue with permission check
1307
- const result = await hasPermission(requiredPermission, false);
1308
- return result;
1309
- }
1310
- else {
1311
- return false;
1312
- }
1313
- }
1314
- catch (signInError) {
1315
- console.error('Failed to auto-sign-in after token parse error:', signInError);
1316
- setIsAutoConnecting(false); // Clear auto-signing state on error
1317
- return false;
1318
- }
1319
- }
1320
- // If autoRetry is false, don't show message again, just return false
1321
- return false;
1322
- }
1323
- if (!currentAit) {
1324
- // Show loading message if AIT is still loading
1325
- if (isAITLoading) {
1326
- setIsLoading(false); // Stop thinking before showing message
1327
- setMessages(prev => [...prev, {
1328
- id: Date.now().toString(),
1329
- content: walletTextUtils.getWalletText('Loading your wallet configuration... Please wait a moment.', serviceId),
1330
- role: 'assistant',
1331
- timestamp: new Date().toISOString()
1332
- }]);
1333
- return false;
1334
- }
1335
- // If AIT is not loaded but we have a token, try to refresh it once
1336
- if (currentToken && !isAITLoading) {
1337
- try {
1338
- await refreshAIT();
1339
- // Wait for AIT to be loaded with polling
1340
- let attempts = 0;
1341
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
1342
- while (!aitRef.current && attempts < maxAttempts) {
1343
- await new Promise(resolve => setTimeout(resolve, 2000));
1344
- attempts++;
1345
- }
1346
- // Check again after refresh
1347
- if (!aitRef.current) {
1348
- setIsLoading(false); // Stop thinking before showing message
1349
- setMessages(prev => [...prev, {
1350
- id: Date.now().toString(),
1351
- content: walletTextUtils.getWalletText('No AIT found for your wallet. Please click the settings button (⚙️) to configure your AIT permissions.', serviceId),
1352
- role: 'assistant',
1353
- timestamp: new Date().toISOString()
1354
- }]);
1355
- return false;
1356
- }
1357
- }
1358
- catch (error) {
1359
- console.error('Failed to refresh AIT during permission check:', error);
1360
- setIsLoading(false); // Stop thinking before showing message
1361
- setMessages(prev => [...prev, {
1362
- id: Date.now().toString(),
1363
- content: 'No AIT found for your wallet. Please click the settings button (⚙️) to configure your AIT permissions.',
1364
- role: 'assistant',
1365
- timestamp: new Date().toISOString()
1366
- }]);
1367
- return false;
1368
- }
1369
- }
1370
- else {
1371
- setIsLoading(false); // Stop thinking before showing message
1372
- setMessages(prev => [...prev, {
1373
- id: Date.now().toString(),
1374
- content: 'No AIT found for your wallet. Please click the settings button (⚙️) to configure your AIT permissions.',
1375
- role: 'assistant',
1376
- timestamp: new Date().toISOString()
1377
- }]);
1378
- return false;
1379
- }
1380
- }
1381
- if (availablePermissions.length === 0) {
1382
- setIsLoading(false); // Stop thinking before showing message
1383
- setMessages(prev => [...prev, {
1384
- id: Date.now().toString(),
1385
- content: `No permissions available for your current identity provider. Please check your service configuration or contact support. Service ID: ${serviceId}, Permission Group: ${permissionGroup || 'None'}`,
1386
- role: 'assistant',
1387
- timestamp: new Date().toISOString()
1388
- }]);
1389
- return false;
1390
- }
1391
- const checkToolPermissionResult = await nxtlinqApi.agent.checkToolPermission({
1150
+ const authorization = await authorizeTextFrontendTool({
1151
+ api: nxtlinqApi,
1152
+ serviceId,
1153
+ toolName: requiredPermission,
1154
+ snapshot: {
1155
+ walletAddress: currentHitAddress,
1156
+ walletToken: currentToken,
1157
+ // Legacy AIT issuance is unscoped (`externalId=''`). Do not impose a
1158
+ // wallet-address subject until issuance/migration supports it.
1159
+ externalId: undefined,
1160
+ requireWalletIDVVerification,
1161
+ loading: isAITLoading || isAutoConnecting,
1162
+ },
1392
1163
  ...authFields(),
1393
- aitToken: currentToken,
1394
- controller: currentHitAddress,
1395
- toolName: requiredPermission
1164
+ customUsername: (!requireWalletIDVVerification && customUsername)
1165
+ ? getFinalCustomUsername(customUsername)
1166
+ : undefined,
1396
1167
  });
1397
- if ('error' in checkToolPermissionResult) {
1398
- // Check if AIT is still loading or if we just auto-connected
1399
- if (isAITLoading || isAutoConnecting) {
1400
- setIsLoading(false); // Stop thinking before showing message
1401
- setMessages(prev => [...prev, {
1402
- id: Date.now().toString(),
1403
- content: walletTextUtils.getWalletText('Loading your wallet configuration... Please wait a moment.', serviceId),
1404
- role: 'assistant',
1405
- timestamp: new Date().toISOString()
1406
- }]);
1407
- return false;
1408
- }
1409
- // Refresh service permissions before judging; use latest result to decide message
1410
- const freshAvailablePermissions = await fetchAvailablePermissions();
1411
- const latestAvailablePermissions = freshAvailablePermissions ?? availablePermissions;
1412
- const requiredPermission = checkToolPermissionResult.requiredPermission;
1413
- const isInServiceList = latestAvailablePermissions.some((p) => p.label === requiredPermission);
1414
- // Permission not in service's available list → "not available for your current identity provider"
1415
- if (!isInServiceList) {
1416
- setIsLoading(false); // Stop thinking before showing message
1417
- setMessages(prev => [...prev, {
1418
- id: Date.now().toString(),
1419
- content: `This permission (${requiredPermission}) is not available for your current identity provider.`,
1420
- role: 'assistant',
1421
- timestamp: new Date().toISOString()
1422
- }]);
1423
- return false;
1424
- }
1425
- // AIT loaded but permissions empty (no AIT found)
1426
- if (currentAit && currentPermissions.length === 0) {
1427
- setIsLoading(false); // Stop thinking before showing message
1428
- setMessages(prev => [...prev, {
1429
- id: Date.now().toString(),
1430
- content: 'No AIT found for your wallet. Please click the settings button (⚙️) to configure your AIT permissions.',
1431
- role: 'assistant',
1432
- timestamp: new Date().toISOString()
1433
- }]);
1434
- return false;
1435
- }
1436
- // User has AIT but hasn't enabled this permission → prompt to enable
1437
- setIsLoading(false); // Stop thinking before showing message
1438
- const permissionMsg = `You don't have the required AIT permission: ${requiredPermission}. Would you like to enable AIT permission?`;
1439
- setMessages(prev => [
1440
- ...prev,
1441
- {
1442
- id: Date.now().toString(),
1443
- content: permissionMsg,
1444
- role: 'assistant',
1445
- timestamp: new Date().toISOString(),
1446
- button: 'enableAIT',
1447
- metadata: { requiredPermission }
1448
- }
1449
- ]);
1450
- return false;
1451
- }
1452
- return true;
1168
+ if (authorization.status === 'allowed')
1169
+ return true;
1170
+ setIsLoading(false);
1171
+ const recovery = {
1172
+ id: Date.now().toString(),
1173
+ content: 'Authorization is temporarily unavailable. Please try again.',
1174
+ role: 'assistant',
1175
+ timestamp: new Date().toISOString(),
1176
+ };
1177
+ switch (authorization.status) {
1178
+ case 'wallet_disconnected':
1179
+ recovery.content = walletTextUtils.getWalletText('Please connect your HIT wallet to continue.', serviceId);
1180
+ recovery.button = 'connectWallet';
1181
+ break;
1182
+ case 'wallet_signed_out':
1183
+ recovery.content = walletTextUtils.getWalletText('Please sign in with your HIT wallet to continue.', serviceId);
1184
+ recovery.button = 'signIn';
1185
+ break;
1186
+ case 'wallet_session_invalid':
1187
+ recovery.content = walletTextUtils.getWalletText('Invalid wallet session. Please sign in again.', serviceId);
1188
+ recovery.button = 'signIn';
1189
+ break;
1190
+ case 'wallet_verification_required':
1191
+ recovery.content = 'Please verify your wallet identity before using this tool.';
1192
+ recovery.button = 'verifyWallet';
1193
+ setIsPermissionFormOpen(true);
1194
+ setShowPermissionForm(true);
1195
+ break;
1196
+ case 'ait_not_found':
1197
+ recovery.content = 'No AIT found for your wallet. Please open settings to configure your AIT.';
1198
+ break;
1199
+ case 'permission_denied':
1200
+ recovery.content = `You don't have the required AIT permission: ${authorization.requiredPermission}. Would you like to enable AIT permission?`;
1201
+ recovery.button = 'enableAIT';
1202
+ recovery.metadata = { requiredPermission: authorization.requiredPermission };
1203
+ break;
1204
+ case 'authorization_unavailable':
1205
+ break;
1206
+ }
1207
+ setMessages(prev => [...prev, recovery]);
1208
+ return false;
1453
1209
  };
1454
1210
  // AI Model related functions
1455
1211
  const handleModelChange = React.useCallback((modelIndex) => {
@@ -1488,9 +1244,18 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1488
1244
  // Updated sendMessage function to support different AI models and attachments
1489
1245
  const sendMessage = async (content, retryCount = 0, isPresetMessage = false, attachments, clientPipelineOverride) => {
1490
1246
  const hasContent = content.trim() || (attachments && attachments.length > 0);
1491
- if (!hasContent || isLoading)
1247
+ // Recovery retries may run immediately after a completion handler calls
1248
+ // setIsLoading(false), before React has committed that state update.
1249
+ if (!hasContent || (retryCount === 0 && isLoading))
1492
1250
  return;
1251
+ if (retryCount === 0) {
1252
+ // A new user turn supersedes any older interrupted tool request.
1253
+ pendingTextToolRetryRef.current.clear();
1254
+ }
1493
1255
  setPendingAutoTts(null);
1256
+ const turnExternalId = typeof window !== 'undefined'
1257
+ ? localStorage.getItem('walletAddress') || undefined
1258
+ : hitAddressRef.current || undefined;
1494
1259
  const currentModel = getCurrentModel();
1495
1260
  // Initialize with current model, will be updated with actual model from backend response
1496
1261
  let actualModelUsed = currentModel.value;
@@ -1562,7 +1327,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1562
1327
  model: currentModel.value,
1563
1328
  ...authFields(),
1564
1329
  pseudoId: pseudoId,
1565
- externalId: localStorage.getItem('walletAddress') || undefined,
1330
+ externalId: turnExternalId,
1566
1331
  customUserInfo,
1567
1332
  customUsername,
1568
1333
  message: content || (attachments && attachments.length > 0 ? `Uploaded ${attachments.length} file(s)` : ''),
@@ -1776,84 +1541,20 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1776
1541
  const toolUse = response.toolCall.toolUse;
1777
1542
  let toolMsg = '';
1778
1543
  if (onToolUse) {
1779
- let wasAutoConnected = false;
1780
- let wasAutoSignedIn = false;
1781
- // Added: Mark if permission denied due to missing AIT permission
1782
- let permissionDenied = false;
1783
- // Use requiredPermission from response if available, otherwise fall back to toolUse.name
1784
- const permissionToCheck = response?.requiredPermission || toolUse.name;
1785
- const isToolAllowed = await hasPermission(permissionToCheck, true, () => { wasAutoConnected = true; }, () => { wasAutoSignedIn = true; });
1786
- // If currentPermissions does not include permissionToCheck and availablePermissionLabels includes permissionToCheck, it means AIT permission is missing
1787
- if (!isToolAllowed && !permissions.includes(permissionToCheck) && availablePermissions.map(p => p.label).includes(permissionToCheck)) {
1788
- permissionDenied = true;
1789
- }
1544
+ const isToolAllowed = await hasPermission(toolUse.name);
1545
+ // Typed preflight already emitted the only permitted recovery UI.
1790
1546
  if (!isToolAllowed) {
1791
- // If permission denied due to missing AIT permission return
1792
- if (permissionDenied) {
1793
- setIsLoading(false);
1794
- return;
1795
- }
1796
- if (isSemiAutomaticMode) {
1797
- setIsLoading(false);
1798
- setMessages(prev => [...prev, {
1799
- id: Date.now().toString(),
1800
- content: 'Click button to continue using tool',
1801
- role: 'assistant',
1802
- timestamp: new Date().toISOString(),
1803
- button: 'continue'
1804
- }]);
1805
- return;
1806
- }
1807
- else {
1808
- // Only retry for auto-connect/auto-sign-in scenarios
1809
- if (wasAutoConnected && retryCount < 1) {
1810
- // Clear loading state and retry immediately
1811
- setIsLoading(false);
1812
- // Check if wallet is already signed in
1813
- const currentToken = nxtlinqAITServiceAccessTokenRef.current;
1814
- if (!currentToken) {
1815
- // If not signed in, directly retry the message without waiting for AIT
1816
- setTimeout(() => {
1817
- sendMessage(content, retryCount + 1, isPresetMessage);
1818
- }, 2000);
1819
- }
1820
- else {
1821
- // If already signed in, wait for AIT to be fully loaded before retrying
1822
- setTimeout(async () => {
1823
- // Wait for AIT to be loaded if needed
1824
- if (!aitRef.current && nxtlinqAITServiceAccessTokenRef.current) {
1825
- await refreshAIT();
1826
- }
1827
- // Wait for AIT to be fully loaded with polling
1828
- let attempts = 0;
1829
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
1830
- while (!aitRef.current && attempts < maxAttempts) {
1831
- await new Promise(resolve => setTimeout(resolve, 2000));
1832
- attempts++;
1833
- }
1834
- // Only retry if AIT is actually loaded
1835
- if (aitRef.current) {
1836
- // Wait a bit more to ensure permissions are also loaded
1837
- await new Promise(resolve => setTimeout(resolve, 3000));
1838
- sendMessage(content, retryCount + 1, isPresetMessage);
1839
- }
1840
- }, 2000);
1841
- }
1842
- }
1843
- return;
1844
- }
1845
- }
1846
- if (isSemiAutomaticMode && wasAutoSignedIn) {
1547
+ pendingTextToolRetryRef.current.set({
1548
+ content,
1549
+ isPresetMessage,
1550
+ attachments,
1551
+ });
1847
1552
  setIsLoading(false);
1848
- setMessages(prev => [...prev, {
1849
- id: Date.now().toString(),
1850
- content: 'Click button to continue using tool',
1851
- role: 'assistant',
1852
- timestamp: new Date().toISOString(),
1853
- button: 'continue'
1854
- }]);
1855
1553
  return;
1856
1554
  }
1555
+ // Authorization passed. Clear before invoking the host callback so
1556
+ // concurrent completion events cannot execute this tool twice.
1557
+ pendingTextToolRetryRef.current.clear();
1857
1558
  // Create streaming message for tool execution
1858
1559
  const streamingMessageId = `streaming-${Date.now()}`;
1859
1560
  const streamingMessage = {
@@ -2146,6 +1847,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2146
1847
  setIsLoading(false);
2147
1848
  }
2148
1849
  };
1850
+ sendMessageRef.current = sendMessage;
2149
1851
  // Handle submit
2150
1852
  const handleSubmit = async (e, attachments) => {
2151
1853
  e.preventDefault();
@@ -2635,6 +2337,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2635
2337
  setShowPermissionForm(false);
2636
2338
  setIsPermissionFormOpen(false);
2637
2339
  await refreshAIT(true);
2340
+ await retryPendingTextTool();
2638
2341
  }
2639
2342
  catch (error) {
2640
2343
  console.error('Failed to generate AIT:', error);
@@ -2713,7 +2416,8 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2713
2416
  }
2714
2417
  setIsLoading(false);
2715
2418
  showSuccess(walletTextUtils.getWalletText('Wallet verification completed successfully! Your wallet is now verified and ready to use.', serviceId));
2716
- refreshAIT();
2419
+ await refreshAIT();
2420
+ await retryPendingTextTool();
2717
2421
  return;
2718
2422
  }
2719
2423
  showError(verifyWalletResponse.error);
@@ -2753,7 +2457,8 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2753
2457
  }
2754
2458
  setIsLoading(false);
2755
2459
  showSuccess('Wallet verification completed successfully! Your wallet is now verified and ready to use.');
2756
- refreshAIT();
2460
+ await refreshAIT();
2461
+ await retryPendingTextTool();
2757
2462
  return;
2758
2463
  }
2759
2464
  catch (error) {
@@ -2793,6 +2498,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2793
2498
  await refreshAIT();
2794
2499
  setIsLoading(false);
2795
2500
  showSuccess(walletTextUtils.getWalletText('Wallet verification completed successfully! Your wallet is now verified and ready to use.', serviceId));
2501
+ await retryPendingTextTool();
2796
2502
  return;
2797
2503
  }
2798
2504
  // Handle specific error messages
@@ -2809,6 +2515,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2809
2515
  await refreshAIT();
2810
2516
  setIsLoading(false);
2811
2517
  showSuccess(walletTextUtils.getWalletText('Wallet verification completed successfully! Your wallet is now verified and ready to use.', serviceId));
2518
+ await retryPendingTextTool();
2812
2519
  return;
2813
2520
  }
2814
2521
  catch (error) {
@@ -2977,6 +2684,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2977
2684
  // Functions
2978
2685
  connectWallet,
2979
2686
  signInWallet,
2687
+ retryPendingTextTool,
2980
2688
  sendMessage,
2981
2689
  handleSubmit,
2982
2690
  handlePresetMessage,