@lvce-editor/chat-view 2.7.0 → 2.9.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.
@@ -1078,6 +1078,7 @@ const {
1078
1078
  set: set$3
1079
1079
  } = create$2(6002);
1080
1080
 
1081
+ const Div$1 = 4;
1081
1082
  const Ol$1 = 49;
1082
1083
 
1083
1084
  const ClientX = 'event.clientX';
@@ -1512,6 +1513,7 @@ const createDefaultState = () => {
1512
1513
  useMockApi: false,
1513
1514
  viewMode: 'list',
1514
1515
  warningCount: 0,
1516
+ webSearchEnabled: true,
1515
1517
  width: 0,
1516
1518
  x: 0,
1517
1519
  y: 0
@@ -2647,7 +2649,8 @@ const deleteSession = async (state, id) => {
2647
2649
  };
2648
2650
 
2649
2651
  const handleClickOpenApiApiKeySettings = async state => {
2650
- await openExternal(state.openApiApiKeysSettingsUrl);
2652
+ // Open the built-in settings editor so the user can inspect or edit their OpenAI API key.
2653
+ await invoke('Main.openUri', 'app://settings.json');
2651
2654
  return state;
2652
2655
  };
2653
2656
 
@@ -3051,8 +3054,6 @@ const getMockOpenRouterAssistantText = async (messages, modelId, openRouterApiBa
3051
3054
  }
3052
3055
  };
3053
3056
 
3054
- const OnFileSystem = 'onFileSystem';
3055
-
3056
3057
  const isPathTraversalAttempt = path => {
3057
3058
  if (!path) {
3058
3059
  return false;
@@ -3069,6 +3070,7 @@ const isPathTraversalAttempt = path => {
3069
3070
  const segments = path.split(/[\\/]/);
3070
3071
  return segments.includes('..');
3071
3072
  };
3073
+
3072
3074
  const normalizeRelativePath = path => {
3073
3075
  const segments = path.split(/[\\/]/).filter(segment => segment && segment !== '.');
3074
3076
  if (segments.length === 0) {
@@ -3076,6 +3078,87 @@ const normalizeRelativePath = path => {
3076
3078
  }
3077
3079
  return segments.join('/');
3078
3080
  };
3081
+
3082
+ const executeListFilesTool = async (args, _options) => {
3083
+ const folderPath = typeof args.path === 'string' && args.path ? args.path : '.';
3084
+ if (isPathTraversalAttempt(folderPath)) {
3085
+ return JSON.stringify({
3086
+ error: 'Access denied: path must be relative and stay within the open workspace folder.'
3087
+ });
3088
+ }
3089
+ const normalizedPath = normalizeRelativePath(folderPath);
3090
+ try {
3091
+ const entries = await invoke('FileSystem.readDirWithFileTypes', normalizedPath);
3092
+ return JSON.stringify({
3093
+ entries,
3094
+ path: normalizedPath
3095
+ });
3096
+ } catch (error) {
3097
+ return JSON.stringify({
3098
+ error: String(error),
3099
+ path: normalizedPath
3100
+ });
3101
+ }
3102
+ };
3103
+
3104
+ const executeReadFileTool = async (args, _options) => {
3105
+ const filePath = typeof args.path === 'string' ? args.path : '';
3106
+ if (!filePath || isPathTraversalAttempt(filePath)) {
3107
+ return JSON.stringify({
3108
+ error: 'Access denied: path must be relative and stay within the open workspace folder.'
3109
+ });
3110
+ }
3111
+ const normalizedPath = normalizeRelativePath(filePath);
3112
+ try {
3113
+ const content = await readFile(normalizedPath);
3114
+ return JSON.stringify({
3115
+ content,
3116
+ path: normalizedPath
3117
+ });
3118
+ } catch (error) {
3119
+ return JSON.stringify({
3120
+ error: String(error),
3121
+ path: normalizedPath
3122
+ });
3123
+ }
3124
+ };
3125
+
3126
+ const OnFileSystem = 'onFileSystem';
3127
+
3128
+ const executeFileSystemCommand = async (method, params, options) => {
3129
+ return executeProvider({
3130
+ assetDir: options.assetDir,
3131
+ event: OnFileSystem,
3132
+ method,
3133
+ noProviderFoundMessage: 'No file system provider found',
3134
+ params,
3135
+ platform: options.platform
3136
+ });
3137
+ };
3138
+
3139
+ const executeWriteFileTool = async (args, options) => {
3140
+ const filePath = typeof args.path === 'string' ? args.path : '';
3141
+ const content = typeof args.content === 'string' ? args.content : '';
3142
+ if (!filePath || isPathTraversalAttempt(filePath)) {
3143
+ return JSON.stringify({
3144
+ error: 'Access denied: path must be relative and stay within the open workspace folder.'
3145
+ });
3146
+ }
3147
+ const normalizedPath = normalizeRelativePath(filePath);
3148
+ try {
3149
+ await executeFileSystemCommand(FileSystemWriteFile, ['file', normalizedPath, content], options);
3150
+ return JSON.stringify({
3151
+ ok: true,
3152
+ path: normalizedPath
3153
+ });
3154
+ } catch (error) {
3155
+ return JSON.stringify({
3156
+ error: String(error),
3157
+ path: normalizedPath
3158
+ });
3159
+ }
3160
+ };
3161
+
3079
3162
  const parseToolArguments = rawArguments => {
3080
3163
  if (typeof rawArguments !== 'string') {
3081
3164
  return {};
@@ -3090,16 +3173,23 @@ const parseToolArguments = rawArguments => {
3090
3173
  return {};
3091
3174
  }
3092
3175
  };
3093
- const executeFileSystemCommand = async (method, params, options) => {
3094
- return executeProvider({
3095
- assetDir: options.assetDir,
3096
- event: OnFileSystem,
3097
- method,
3098
- noProviderFoundMessage: 'No file system provider found',
3099
- params,
3100
- platform: options.platform
3176
+
3177
+ const executeChatTool = async (name, rawArguments, options) => {
3178
+ const args = parseToolArguments(rawArguments);
3179
+ if (name === 'read_file') {
3180
+ return executeReadFileTool(args);
3181
+ }
3182
+ if (name === 'write_file') {
3183
+ return executeWriteFileTool(args, options);
3184
+ }
3185
+ if (name === 'list_files') {
3186
+ return executeListFilesTool(args);
3187
+ }
3188
+ return JSON.stringify({
3189
+ error: `Unknown tool: ${name}`
3101
3190
  });
3102
3191
  };
3192
+
3103
3193
  const getBasicChatTools = () => {
3104
3194
  return [{
3105
3195
  function: {
@@ -3157,76 +3247,6 @@ const getBasicChatTools = () => {
3157
3247
  type: 'function'
3158
3248
  }];
3159
3249
  };
3160
- const executeChatTool = async (name, rawArguments, options) => {
3161
- const args = parseToolArguments(rawArguments);
3162
- if (name === 'read_file') {
3163
- const filePath = typeof args.path === 'string' ? args.path : '';
3164
- if (!filePath || isPathTraversalAttempt(filePath)) {
3165
- return JSON.stringify({
3166
- error: 'Access denied: path must be relative and stay within the open workspace folder.'
3167
- });
3168
- }
3169
- const normalizedPath = normalizeRelativePath(filePath);
3170
- try {
3171
- const content = await readFile(normalizedPath);
3172
- return JSON.stringify({
3173
- content,
3174
- path: normalizedPath
3175
- });
3176
- } catch (error) {
3177
- return JSON.stringify({
3178
- error: String(error),
3179
- path: normalizedPath
3180
- });
3181
- }
3182
- }
3183
- if (name === 'write_file') {
3184
- const filePath = typeof args.path === 'string' ? args.path : '';
3185
- const content = typeof args.content === 'string' ? args.content : '';
3186
- if (!filePath || isPathTraversalAttempt(filePath)) {
3187
- return JSON.stringify({
3188
- error: 'Access denied: path must be relative and stay within the open workspace folder.'
3189
- });
3190
- }
3191
- const normalizedPath = normalizeRelativePath(filePath);
3192
- try {
3193
- await executeFileSystemCommand(FileSystemWriteFile, ['file', normalizedPath, content], options);
3194
- return JSON.stringify({
3195
- ok: true,
3196
- path: normalizedPath
3197
- });
3198
- } catch (error) {
3199
- return JSON.stringify({
3200
- error: String(error),
3201
- path: normalizedPath
3202
- });
3203
- }
3204
- }
3205
- if (name === 'list_files') {
3206
- const folderPath = typeof args.path === 'string' && args.path ? args.path : '.';
3207
- if (isPathTraversalAttempt(folderPath)) {
3208
- return JSON.stringify({
3209
- error: 'Access denied: path must be relative and stay within the open workspace folder.'
3210
- });
3211
- }
3212
- const normalizedPath = normalizeRelativePath(folderPath);
3213
- try {
3214
- const entries = await invoke('FileSystem.readDirWithFileTypes', normalizedPath);
3215
- return JSON.stringify({
3216
- entries,
3217
- path: normalizedPath
3218
- });
3219
- } catch (error) {
3220
- return JSON.stringify({
3221
- error: String(error),
3222
- path: normalizedPath
3223
- });
3224
- }
3225
- }
3226
- return JSON.stringify({
3227
- error: `Unknown tool: ${name}`
3228
- });
3229
- };
3230
3250
 
3231
3251
  const getClientRequestIdHeader = () => {
3232
3252
  return {
@@ -3286,7 +3306,8 @@ const getOpenAiTools = tools => {
3286
3306
  };
3287
3307
  });
3288
3308
  };
3289
- const getOpenAiParams = (input, modelId, stream, includeObfuscation, tools, previousResponseId) => {
3309
+ const getOpenAiParams = (input, modelId, stream, includeObfuscation, tools, webSearchEnabled, previousResponseId) => {
3310
+ const openAiTools = getOpenAiTools(tools);
3290
3311
  return {
3291
3312
  input,
3292
3313
  model: modelId,
@@ -3302,7 +3323,9 @@ const getOpenAiParams = (input, modelId, stream, includeObfuscation, tools, prev
3302
3323
  previous_response_id: previousResponseId
3303
3324
  } : {}),
3304
3325
  tool_choice: 'auto',
3305
- tools: getOpenAiTools(tools)
3326
+ tools: webSearchEnabled ? [...openAiTools, {
3327
+ type: 'web_search'
3328
+ }] : openAiTools
3306
3329
  };
3307
3330
  };
3308
3331
  const getStreamChunkText = content => {
@@ -3320,6 +3343,47 @@ const getStreamChunkText = content => {
3320
3343
  return typeof text === 'string' ? text : '';
3321
3344
  }).join('');
3322
3345
  };
3346
+ const getShortToolErrorMessage = error => {
3347
+ const trimmed = error.trim().replace(/^Error:\s*/, '');
3348
+ const firstLine = trimmed.split('\n')[0];
3349
+ if (firstLine.length <= 80) {
3350
+ return firstLine;
3351
+ }
3352
+ return `${firstLine.slice(0, 77)}...`;
3353
+ };
3354
+ const getToolCallExecutionStatus = content => {
3355
+ let parsed;
3356
+ try {
3357
+ parsed = JSON.parse(content);
3358
+ } catch {
3359
+ return {
3360
+ errorMessage: 'Invalid tool output',
3361
+ status: 'error'
3362
+ };
3363
+ }
3364
+ if (!parsed || typeof parsed !== 'object') {
3365
+ return {
3366
+ errorMessage: 'Invalid tool output',
3367
+ status: 'error'
3368
+ };
3369
+ }
3370
+ const rawError = Reflect.get(parsed, 'error');
3371
+ if (typeof rawError !== 'string' || !rawError.trim()) {
3372
+ return {
3373
+ status: 'success'
3374
+ };
3375
+ }
3376
+ const errorMessage = getShortToolErrorMessage(rawError);
3377
+ if (/not[\s_-]?found|enoent/i.test(errorMessage)) {
3378
+ return {
3379
+ status: 'not-found'
3380
+ };
3381
+ }
3382
+ return {
3383
+ errorMessage,
3384
+ status: 'error'
3385
+ };
3386
+ };
3323
3387
  const getResponseOutputText = parsed => {
3324
3388
  if (!parsed || typeof parsed !== 'object') {
3325
3389
  return '';
@@ -3457,12 +3521,29 @@ const updateToolCallAccumulator = (accumulator, chunk) => {
3457
3521
  };
3458
3522
  };
3459
3523
  const getResponseFunctionCallsFromStreamingAccumulator = toolCallAccumulator => {
3460
- return Object.entries(toolCallAccumulator).toSorted((a, b) => Number(a[0]) - Number(b[0])).map(entry => entry[1]).filter(toolCall => typeof toolCall.id === 'string' && !!toolCall.id && !!toolCall.name).map(toolCall => ({
3524
+ return Object.entries(toolCallAccumulator).toSorted((a, b) => a[0].localeCompare(b[0])).map(entry => entry[1]).filter(toolCall => typeof toolCall.id === 'string' && !!toolCall.id && !!toolCall.name).map(toolCall => ({
3461
3525
  arguments: toolCall.arguments,
3462
3526
  callId: toolCall.id,
3463
3527
  name: toolCall.name
3464
3528
  }));
3465
3529
  };
3530
+ const getStreamingToolCallKey = value => {
3531
+ if (!value || typeof value !== 'object') {
3532
+ return undefined;
3533
+ }
3534
+ const outputIndex = Reflect.get(value, 'output_index');
3535
+ if (typeof outputIndex === 'number') {
3536
+ return String(outputIndex);
3537
+ }
3538
+ if (typeof outputIndex === 'string' && outputIndex) {
3539
+ return outputIndex;
3540
+ }
3541
+ const itemId = Reflect.get(value, 'item_id');
3542
+ if (typeof itemId === 'string' && itemId) {
3543
+ return itemId;
3544
+ }
3545
+ return undefined;
3546
+ };
3466
3547
  const parseOpenApiStream = async (response, onTextChunk, onToolCallsChunk, onDataEvent) => {
3467
3548
  if (!response.body) {
3468
3549
  return {
@@ -3482,7 +3563,7 @@ const parseOpenApiStream = async (response, onTextChunk, onToolCallsChunk, onDat
3482
3563
  if (!onToolCallsChunk) {
3483
3564
  return;
3484
3565
  }
3485
- const toolCalls = Object.entries(toolCallAccumulator).toSorted((a, b) => Number(a[0]) - Number(b[0])).map(entry => entry[1]).filter(toolCall => !!toolCall.name);
3566
+ const toolCalls = Object.entries(toolCallAccumulator).toSorted((a, b) => a[0].localeCompare(b[0])).map(entry => entry[1]).filter(toolCall => !!toolCall.name);
3486
3567
  if (toolCalls.length === 0) {
3487
3568
  return;
3488
3569
  }
@@ -3530,9 +3611,9 @@ const parseOpenApiStream = async (response, onTextChunk, onToolCallsChunk, onDat
3530
3611
  return;
3531
3612
  }
3532
3613
  if (eventType === 'response.output_item.added') {
3533
- const outputIndex = Reflect.get(parsed, 'output_index');
3614
+ const toolCallKey = getStreamingToolCallKey(parsed);
3534
3615
  const item = Reflect.get(parsed, 'item');
3535
- if (typeof outputIndex !== 'number' || !item || typeof item !== 'object') {
3616
+ if (!toolCallKey || !item || typeof item !== 'object') {
3536
3617
  return;
3537
3618
  }
3538
3619
  const itemType = Reflect.get(item, 'type');
@@ -3551,15 +3632,15 @@ const parseOpenApiStream = async (response, onTextChunk, onToolCallsChunk, onDat
3551
3632
  };
3552
3633
  toolCallAccumulator = {
3553
3634
  ...toolCallAccumulator,
3554
- [outputIndex]: next
3635
+ [toolCallKey]: next
3555
3636
  };
3556
3637
  await emitToolCallAccumulator();
3557
3638
  return;
3558
3639
  }
3559
3640
  if (eventType === 'response.output_item.done') {
3560
- const outputIndex = Reflect.get(parsed, 'output_index');
3641
+ const toolCallKey = getStreamingToolCallKey(parsed);
3561
3642
  const item = Reflect.get(parsed, 'item');
3562
- if (typeof outputIndex !== 'number' || !item || typeof item !== 'object') {
3643
+ if (!toolCallKey || !item || typeof item !== 'object') {
3563
3644
  return;
3564
3645
  }
3565
3646
  const itemType = Reflect.get(item, 'type');
@@ -3569,13 +3650,13 @@ const parseOpenApiStream = async (response, onTextChunk, onToolCallsChunk, onDat
3569
3650
  const callId = Reflect.get(item, 'call_id');
3570
3651
  const name = Reflect.get(item, 'name');
3571
3652
  const rawArguments = Reflect.get(item, 'arguments');
3572
- const current = toolCallAccumulator[outputIndex] || {
3653
+ const current = toolCallAccumulator[toolCallKey] || {
3573
3654
  arguments: '',
3574
3655
  name: ''
3575
3656
  };
3576
3657
  toolCallAccumulator = {
3577
3658
  ...toolCallAccumulator,
3578
- [outputIndex]: {
3659
+ [toolCallKey]: {
3579
3660
  arguments: typeof rawArguments === 'string' ? rawArguments : current.arguments,
3580
3661
  ...(typeof callId === 'string' ? {
3581
3662
  id: callId
@@ -3589,11 +3670,11 @@ const parseOpenApiStream = async (response, onTextChunk, onToolCallsChunk, onDat
3589
3670
  return;
3590
3671
  }
3591
3672
  if (eventType === 'response.function_call_arguments.delta' || eventType === 'response.function_call_arguments.done') {
3592
- const outputIndex = Reflect.get(parsed, 'output_index');
3593
- if (typeof outputIndex !== 'number') {
3673
+ const toolCallKey = getStreamingToolCallKey(parsed);
3674
+ if (!toolCallKey) {
3594
3675
  return;
3595
3676
  }
3596
- const current = toolCallAccumulator[outputIndex] || {
3677
+ const current = toolCallAccumulator[toolCallKey] || {
3597
3678
  arguments: '',
3598
3679
  name: ''
3599
3680
  };
@@ -3612,7 +3693,7 @@ const parseOpenApiStream = async (response, onTextChunk, onToolCallsChunk, onDat
3612
3693
  };
3613
3694
  toolCallAccumulator = {
3614
3695
  ...toolCallAccumulator,
3615
- [outputIndex]: next
3696
+ [toolCallKey]: next
3616
3697
  };
3617
3698
  await emitToolCallAccumulator();
3618
3699
  return;
@@ -3746,7 +3827,8 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
3746
3827
  onEventStreamFinished,
3747
3828
  onTextChunk,
3748
3829
  onToolCallsChunk,
3749
- stream
3830
+ stream,
3831
+ webSearchEnabled = false
3750
3832
  } = options ?? {
3751
3833
  stream: false
3752
3834
  };
@@ -3761,7 +3843,7 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
3761
3843
  let response;
3762
3844
  try {
3763
3845
  response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
3764
- body: JSON.stringify(getOpenAiParams(openAiInput, modelId, stream, includeObfuscation, tools, previousResponseId)),
3846
+ body: JSON.stringify(getOpenAiParams(openAiInput, modelId, stream, includeObfuscation, tools, webSearchEnabled, previousResponseId)),
3765
3847
  headers: {
3766
3848
  Authorization: `Bearer ${openApiApiKey}`,
3767
3849
  'Content-Type': 'application/json',
@@ -3804,6 +3886,37 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
3804
3886
  if (streamResult.responseId) {
3805
3887
  previousResponseId = streamResult.responseId;
3806
3888
  }
3889
+ if (streamResult.responseFunctionCalls.length > 0) {
3890
+ openAiInput.length = 0;
3891
+ const executedToolCalls = [];
3892
+ for (const toolCall of streamResult.responseFunctionCalls) {
3893
+ const content = await executeChatTool(toolCall.name, toolCall.arguments, {
3894
+ assetDir,
3895
+ platform
3896
+ });
3897
+ const executionStatus = getToolCallExecutionStatus(content);
3898
+ executedToolCalls.push({
3899
+ arguments: toolCall.arguments,
3900
+ ...(executionStatus.errorMessage ? {
3901
+ errorMessage: executionStatus.errorMessage
3902
+ } : {}),
3903
+ id: toolCall.callId,
3904
+ name: toolCall.name,
3905
+ ...(executionStatus.status ? {
3906
+ status: executionStatus.status
3907
+ } : {})
3908
+ });
3909
+ openAiInput.push({
3910
+ call_id: toolCall.callId,
3911
+ output: content,
3912
+ type: 'function_call_output'
3913
+ });
3914
+ }
3915
+ if (onToolCallsChunk && executedToolCalls.length > 0) {
3916
+ await onToolCallsChunk(executedToolCalls);
3917
+ }
3918
+ continue;
3919
+ }
3807
3920
  if (onEventStreamFinished) {
3808
3921
  await onEventStreamFinished();
3809
3922
  }
@@ -3834,17 +3947,33 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
3834
3947
  const responseFunctionCalls = getResponseFunctionCalls(parsed);
3835
3948
  if (responseFunctionCalls.length > 0) {
3836
3949
  openAiInput.length = 0;
3950
+ const executedToolCalls = [];
3837
3951
  for (const toolCall of responseFunctionCalls) {
3838
3952
  const content = await executeChatTool(toolCall.name, toolCall.arguments, {
3839
3953
  assetDir,
3840
3954
  platform
3841
3955
  });
3956
+ const executionStatus = getToolCallExecutionStatus(content);
3957
+ executedToolCalls.push({
3958
+ arguments: toolCall.arguments,
3959
+ ...(executionStatus.errorMessage ? {
3960
+ errorMessage: executionStatus.errorMessage
3961
+ } : {}),
3962
+ id: toolCall.callId,
3963
+ name: toolCall.name,
3964
+ ...(executionStatus.status ? {
3965
+ status: executionStatus.status
3966
+ } : {})
3967
+ });
3842
3968
  openAiInput.push({
3843
3969
  call_id: toolCall.callId,
3844
3970
  output: content,
3845
3971
  type: 'function_call_output'
3846
3972
  });
3847
3973
  }
3974
+ if (onToolCallsChunk && executedToolCalls.length > 0) {
3975
+ await onToolCallsChunk(executedToolCalls);
3976
+ }
3848
3977
  continue;
3849
3978
  }
3850
3979
  const outputText = getResponseOutputText(parsed);
@@ -3873,6 +4002,7 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
3873
4002
  const toolCalls = Reflect.get(message, 'tool_calls');
3874
4003
  if (Array.isArray(toolCalls) && toolCalls.length > 0) {
3875
4004
  openAiInput.length = 0;
4005
+ const executedToolCalls = [];
3876
4006
  for (const toolCall of toolCalls) {
3877
4007
  if (!toolCall || typeof toolCall !== 'object') {
3878
4008
  continue;
@@ -3888,12 +4018,29 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
3888
4018
  assetDir,
3889
4019
  platform
3890
4020
  }) : '{}';
4021
+ if (typeof name === 'string') {
4022
+ const executionStatus = getToolCallExecutionStatus(content);
4023
+ executedToolCalls.push({
4024
+ arguments: typeof rawArguments === 'string' ? rawArguments : '',
4025
+ ...(executionStatus.errorMessage ? {
4026
+ errorMessage: executionStatus.errorMessage
4027
+ } : {}),
4028
+ id,
4029
+ name,
4030
+ ...(executionStatus.status ? {
4031
+ status: executionStatus.status
4032
+ } : {})
4033
+ });
4034
+ }
3891
4035
  openAiInput.push({
3892
4036
  call_id: id,
3893
4037
  output: content,
3894
4038
  type: 'function_call_output'
3895
4039
  });
3896
4040
  }
4041
+ if (onToolCallsChunk && executedToolCalls.length > 0) {
4042
+ await onToolCallsChunk(executedToolCalls);
4043
+ }
3897
4044
  continue;
3898
4045
  }
3899
4046
  const content = Reflect.get(message, 'content');
@@ -3920,6 +4067,12 @@ const getOpenApiErrorMessage = errorResult => {
3920
4067
  const errorMessage = errorResult.errorMessage?.trim();
3921
4068
  const hasErrorCode = typeof errorResult.errorCode === 'string' && errorResult.errorCode.length > 0;
3922
4069
  const hasErrorType = typeof errorResult.errorType === 'string' && errorResult.errorType.length > 0;
4070
+
4071
+ // Provide a concise, user-friendly message when OpenAI reports an invalid API key.
4072
+ if (errorResult.errorCode === 'invalid_api_key') {
4073
+ const status = typeof errorResult.statusCode === 'number' ? errorResult.statusCode : 401;
4074
+ return `OpenAI request failed (status ${status}): Invalid API key. Please verify your OpenAI API key in Chat settings.`;
4075
+ }
3923
4076
  if (errorResult.statusCode === 429) {
3924
4077
  let prefix = 'OpenAI rate limit exceeded (429)';
3925
4078
  if (hasErrorCode) {
@@ -4283,7 +4436,8 @@ const getAiResponse = async ({
4283
4436
  selectedModelId,
4284
4437
  streamingEnabled = false,
4285
4438
  useMockApi,
4286
- userText
4439
+ userText,
4440
+ webSearchEnabled = false
4287
4441
  }) => {
4288
4442
  let text = '';
4289
4443
  const usesOpenApiModel = isOpenApiModel(selectedModelId, models);
@@ -4314,7 +4468,8 @@ const getAiResponse = async ({
4314
4468
  ...(onToolCallsChunk ? {
4315
4469
  onToolCallsChunk
4316
4470
  } : {}),
4317
- stream: streamingEnabled
4471
+ stream: streamingEnabled,
4472
+ webSearchEnabled
4318
4473
  });
4319
4474
  if (result.type === 'success') {
4320
4475
  const {
@@ -4703,7 +4858,8 @@ const handleSubmit = async state => {
4703
4858
  sessions,
4704
4859
  streamingEnabled,
4705
4860
  useMockApi,
4706
- viewMode
4861
+ viewMode,
4862
+ webSearchEnabled
4707
4863
  } = state;
4708
4864
  const userText = composerValue.trim();
4709
4865
  if (!userText) {
@@ -4847,7 +5003,8 @@ const handleSubmit = async state => {
4847
5003
  selectedModelId,
4848
5004
  streamingEnabled,
4849
5005
  useMockApi,
4850
- userText
5006
+ userText,
5007
+ webSearchEnabled
4851
5008
  });
4852
5009
  const {
4853
5010
  latestState
@@ -5168,6 +5325,11 @@ const handleKeyDown = async (state, key, shiftKey) => {
5168
5325
  return handleSubmit(submitState);
5169
5326
  };
5170
5327
 
5328
+ const handleMessagesContextMenu = async state => {
5329
+ await invoke('ContextMenu.show', 1234);
5330
+ return state;
5331
+ };
5332
+
5171
5333
  const handleModelChange = async (state, value) => {
5172
5334
  return {
5173
5335
  ...state,
@@ -5510,6 +5672,34 @@ const getCss = (composerHeight, listItemHeight, chatMessageFontSize, chatMessage
5510
5672
  --ChatMessageFontSize: ${chatMessageFontSize}px;
5511
5673
  --ChatMessageLineHeight: ${chatMessageLineHeight}px;
5512
5674
  --ChatMessageFontFamily: ${chatMessageFontFamily};
5675
+ }
5676
+
5677
+ .ChatToolCalls {
5678
+ position: relative;
5679
+ border: 1px solid var(--vscode-editorWidget-border);
5680
+ border-radius: 4px;
5681
+ margin-bottom: 8px;
5682
+ padding: 10px 8px 6px;
5683
+ background: var(--vscode-editorWidget-background);
5684
+ }
5685
+
5686
+ .ChatToolCallsLabel {
5687
+ position: absolute;
5688
+ top: -8px;
5689
+ left: 8px;
5690
+ padding: 0 4px;
5691
+ border-radius: 3px;
5692
+ background: var(--vscode-editor-background);
5693
+ color: var(--vscode-descriptionForeground);
5694
+ font-size: 10px;
5695
+ line-height: 14px;
5696
+ text-transform: lowercase;
5697
+ letter-spacing: 0.02em;
5698
+ }
5699
+
5700
+ .ChatToolCallReadFileLink {
5701
+ color: var(--vscode-textLink-foreground);
5702
+ text-decoration: underline;
5513
5703
  }`;
5514
5704
  };
5515
5705
 
@@ -5577,6 +5767,9 @@ const ChatListItemLabel = 'ChatListItemLabel';
5577
5767
  const Markdown = 'Markdown';
5578
5768
  const Message = 'Message';
5579
5769
  const ChatMessageContent = 'ChatMessageContent';
5770
+ const ChatToolCalls = 'ChatToolCalls';
5771
+ const ChatToolCallsLabel = 'ChatToolCallsLabel';
5772
+ const ChatToolCallReadFileLink = 'ChatToolCallReadFileLink';
5580
5773
  const ChatOrderedList = 'ChatOrderedList';
5581
5774
  const ChatOrderedListItem = 'ChatOrderedListItem';
5582
5775
  const MessageUser = 'MessageUser';
@@ -5607,6 +5800,7 @@ const HandleChatListScroll = 21;
5607
5800
  const HandleMessagesScroll = 22;
5608
5801
  const HandleClickSessionDebug = 23;
5609
5802
  const HandleClickReadFile = 24;
5803
+ const HandleMessagesContextMenu = 25;
5610
5804
 
5611
5805
  const getModelLabel = model => {
5612
5806
  if (model.provider === 'openRouter') {
@@ -5800,6 +5994,31 @@ const getChatHeaderDomDetailMode = selectedSessionTitle => {
5800
5994
  }, text(selectedSessionTitle), ...getChatHeaderActionsDom()];
5801
5995
  };
5802
5996
 
5997
+ const getMessageNodeDom = node => {
5998
+ if (node.type === 'text') {
5999
+ return [{
6000
+ childCount: 1,
6001
+ className: Markdown,
6002
+ type: P
6003
+ }, text(node.text)];
6004
+ }
6005
+ return [{
6006
+ childCount: node.items.length,
6007
+ className: ChatOrderedList,
6008
+ type: Ol
6009
+ }, ...node.items.flatMap(item => {
6010
+ return [{
6011
+ childCount: 1,
6012
+ className: ChatOrderedListItem,
6013
+ type: Li
6014
+ }, text(item.text)];
6015
+ })];
6016
+ };
6017
+
6018
+ const getMessageContentDom = nodes => {
6019
+ return nodes.flatMap(getMessageNodeDom);
6020
+ };
6021
+
5803
6022
  const getMissingApiKeyDom = ({
5804
6023
  getApiKeyText,
5805
6024
  inputName,
@@ -5921,8 +6140,10 @@ const getToolCallArgumentPreview = rawArguments => {
5921
6140
  return rawArguments;
5922
6141
  };
5923
6142
 
6143
+ const RE_QUERY_OR_HASH = /[?#].*$/;
6144
+ const RE_TRAILING_SLASH = /\/$/;
5924
6145
  const getFileNameFromUri = uri => {
5925
- const stripped = uri.replace(/[?#].*$/, '').replace(/\/$/, '');
6146
+ const stripped = uri.replace(RE_QUERY_OR_HASH, '').replace(RE_TRAILING_SLASH, '');
5926
6147
  const slashIndex = Math.max(stripped.lastIndexOf('/'), stripped.lastIndexOf('\\\\'));
5927
6148
  const fileName = slashIndex === -1 ? stripped : stripped.slice(slashIndex + 1);
5928
6149
  return fileName || uri;
@@ -5946,42 +6167,68 @@ const getReadFileTarget = rawArguments => {
5946
6167
  if (!title) {
5947
6168
  return undefined;
5948
6169
  }
5949
- const clickableUri = uriValue || (pathValue.startsWith('file://') ? pathValue : '');
6170
+ // `read_file` tool calls usually provide a relative `path`; pass it through so UI clicks can open the file.
6171
+ const clickableUri = uriValue || pathValue;
5950
6172
  return {
5951
6173
  clickableUri,
5952
6174
  title
5953
6175
  };
5954
6176
  };
5955
6177
 
6178
+ const getToolCallStatusLabel$1 = toolCall => {
6179
+ if (toolCall.status === 'not-found') {
6180
+ return ' (not-found)';
6181
+ }
6182
+ if (toolCall.status === 'error') {
6183
+ if (toolCall.errorMessage) {
6184
+ return ` (error: ${toolCall.errorMessage})`;
6185
+ }
6186
+ return ' (error)';
6187
+ }
6188
+ return '';
6189
+ };
6190
+
5956
6191
  const getToolCallReadFileVirtualDom = toolCall => {
5957
6192
  const target = getReadFileTarget(toolCall.arguments);
5958
6193
  if (!target) {
5959
6194
  return [];
5960
6195
  }
5961
6196
  const fileName = getFileNameFromUri(target.title);
5962
- const clickableProps = target.clickableUri ? {
6197
+ const toolNameLabel = `${toolCall.name} `;
6198
+ const statusLabel = getToolCallStatusLabel$1(toolCall);
6199
+ const fileNameClickableProps = target.clickableUri ? {
5963
6200
  'data-uri': target.clickableUri,
5964
6201
  onClick: HandleClickReadFile
5965
6202
  } : {};
5966
6203
  return [{
5967
- childCount: 2,
6204
+ childCount: statusLabel ? 4 : 3,
5968
6205
  className: ChatOrderedListItem,
5969
- ...clickableProps,
5970
6206
  title: target.title,
5971
6207
  type: Li
5972
6208
  }, {
5973
6209
  childCount: 0,
5974
6210
  className: FileIcon,
5975
- ...clickableProps,
5976
6211
  type: Div
5977
- }, {
6212
+ }, text(toolNameLabel), {
5978
6213
  childCount: 1,
5979
- ...clickableProps,
5980
- style: 'color: var(--vscode-textLink-foreground); text-decoration: underline;',
6214
+ className: ChatToolCallReadFileLink,
6215
+ ...fileNameClickableProps,
5981
6216
  type: Span
5982
- }, text(fileName)];
6217
+ }, text(fileName), ...(statusLabel ? [text(statusLabel)] : [])];
5983
6218
  };
5984
6219
 
6220
+ const getToolCallStatusLabel = toolCall => {
6221
+ if (toolCall.status === 'not-found') {
6222
+ return ' (not-found)';
6223
+ }
6224
+ if (toolCall.status === 'error') {
6225
+ if (toolCall.errorMessage) {
6226
+ return ` (error: ${toolCall.errorMessage})`;
6227
+ }
6228
+ return ' (error)';
6229
+ }
6230
+ return '';
6231
+ };
5985
6232
  const getToolCallDom = toolCall => {
5986
6233
  if (toolCall.name === 'read_file') {
5987
6234
  const virtualDom = getToolCallReadFileVirtualDom(toolCall);
@@ -5990,7 +6237,7 @@ const getToolCallDom = toolCall => {
5990
6237
  }
5991
6238
  }
5992
6239
  const argumentPreview = getToolCallArgumentPreview(toolCall.arguments);
5993
- const label = `${toolCall.name} ${argumentPreview}`;
6240
+ const label = `${toolCall.name} ${argumentPreview}${getToolCallStatusLabel(toolCall)}`;
5994
6241
  return [{
5995
6242
  childCount: 1,
5996
6243
  className: ChatOrderedListItem,
@@ -6003,6 +6250,14 @@ const getToolCallsDom = message => {
6003
6250
  return [];
6004
6251
  }
6005
6252
  return [{
6253
+ childCount: 2,
6254
+ className: ChatToolCalls,
6255
+ type: Div$1
6256
+ }, {
6257
+ childCount: 1,
6258
+ className: ChatToolCallsLabel,
6259
+ type: Div$1
6260
+ }, text('tools'), {
6006
6261
  childCount: message.toolCalls.length,
6007
6262
  className: ChatOrderedList,
6008
6263
  type: Ol$1
@@ -6066,28 +6321,6 @@ const parseMessageContent = rawMessage => {
6066
6321
  type: 'text'
6067
6322
  }] : nodes;
6068
6323
  };
6069
- const getMessageContentDom = nodes => {
6070
- return nodes.flatMap(node => {
6071
- if (node.type === 'text') {
6072
- return [{
6073
- childCount: 1,
6074
- className: Markdown,
6075
- type: P
6076
- }, text(node.text)];
6077
- }
6078
- return [{
6079
- childCount: node.items.length,
6080
- className: ChatOrderedList,
6081
- type: Ol
6082
- }, ...node.items.flatMap(item => {
6083
- return [{
6084
- childCount: 1,
6085
- className: ChatOrderedListItem,
6086
- type: Li
6087
- }, text(item.text)];
6088
- })];
6089
- });
6090
- };
6091
6324
 
6092
6325
  const getChatMessageDom = (message, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle') => {
6093
6326
  const roleClassName = message.role === 'user' ? MessageUser : MessageAssistant;
@@ -6126,6 +6359,7 @@ const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = ''
6126
6359
  return [{
6127
6360
  childCount: messages.length,
6128
6361
  className: 'ChatMessages',
6362
+ onContextMenu: HandleMessagesContextMenu,
6129
6363
  onScroll: HandleMessagesScroll,
6130
6364
  scrollTop: messagesScrollTop,
6131
6365
  type: Div
@@ -6376,6 +6610,10 @@ const renderEventListeners = () => {
6376
6610
  }, {
6377
6611
  name: HandleMessagesScroll,
6378
6612
  params: ['handleMessagesScroll', 'event.target.scrollTop']
6613
+ }, {
6614
+ name: HandleMessagesContextMenu,
6615
+ params: ['handleMessagesContextMenu'],
6616
+ preventDefault: true
6379
6617
  }, {
6380
6618
  name: HandleFocus,
6381
6619
  params: ['handleInputFocus', TargetName]
@@ -6419,30 +6657,22 @@ const saveState = state => {
6419
6657
  const {
6420
6658
  chatListScrollTop,
6421
6659
  composerValue,
6422
- height,
6423
6660
  messagesScrollTop,
6424
6661
  nextMessageId,
6425
6662
  renamingSessionId,
6426
6663
  selectedModelId,
6427
6664
  selectedSessionId,
6428
- viewMode,
6429
- width,
6430
- x,
6431
- y
6665
+ viewMode
6432
6666
  } = state;
6433
6667
  return {
6434
6668
  chatListScrollTop,
6435
6669
  composerValue,
6436
- height,
6437
6670
  messagesScrollTop,
6438
6671
  nextMessageId,
6439
6672
  renamingSessionId,
6440
6673
  selectedModelId,
6441
6674
  selectedSessionId,
6442
- viewMode,
6443
- width,
6444
- x,
6445
- y
6675
+ viewMode
6446
6676
  };
6447
6677
  };
6448
6678
 
@@ -6519,6 +6749,7 @@ const commandMap = {
6519
6749
  'Chat.handleInput': wrapCommand(handleInput),
6520
6750
  'Chat.handleInputFocus': wrapCommand(handleInputFocus),
6521
6751
  'Chat.handleKeyDown': wrapCommand(handleKeyDown),
6752
+ 'Chat.handleMessagesContextMenu': wrapCommand(handleMessagesContextMenu),
6522
6753
  'Chat.handleMessagesScroll': wrapCommand(handleMessagesScroll),
6523
6754
  'Chat.handleModelChange': wrapCommand(handleModelChange),
6524
6755
  'Chat.handleSubmit': wrapCommand(handleSubmit),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/chat-view",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Chat View Worker",
5
5
  "repository": {
6
6
  "type": "git",